Finish docs

This commit is contained in:
Shmew
2020-06-29 13:24:10 -05:00
parent f3f92fbba0
commit 82b1e86ca3
42 changed files with 1953 additions and 1275 deletions

View File

@@ -43,7 +43,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "web", "web", "{7F29BFE8-02B
.yarnrc = .yarnrc
package.json = package.json
publish.js = publish.js
startServer.js = startServer.js
startDemoServer.js = startDemoServer.js
startTestServer.js = startTestServer.js
webpack.config.js = webpack.config.js
yarn.lock = yarn.lock
EndProjectSection
@@ -89,7 +90,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "signalr-client", "signalr-c
docs\signalr-client\connecting.md = docs\signalr-client\connecting.md
docs\signalr-client\messages.md = docs\signalr-client\messages.md
docs\signalr-client\README.md = docs\signalr-client\README.md
docs\signalr-client\streaming-bidirectional.md = docs\signalr-client\streaming-bidirectional.md
docs\signalr-client\streaming-client.md = docs\signalr-client\streaming-client.md
docs\signalr-client\streaming-server.md = docs\signalr-client\streaming-server.md
EndProjectSection
@@ -97,11 +97,15 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "signalr-server", "signalr-server", "{5336DEEE-6183-44BE-9A9F-DAF2F85C9465}"
ProjectSection(SolutionItems) = preProject
docs\signalr-server\api.md = docs\signalr-server\api.md
docs\signalr-server\giraffe.md = docs\signalr-server\giraffe.md
docs\signalr-server\aspnetcore.md = docs\signalr-server\aspnetcore.md
docs\signalr-server\README.md = docs\signalr-server\README.md
docs\signalr-server\saturn.md = docs\signalr-server\saturn.md
EndProjectSection
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Fable.SignalR.TestServer", "tests\Fable.SignalR.TestServer\Fable.SignalR.TestServer.fsproj", "{6DCEC050-2B9B-4DF7-AE21-D554965EF5A1}"
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Fable.SignalR.TestShared", "tests\Fable.SignalR.TestShared\Fable.SignalR.TestShared.fsproj", "{BE9FFEDE-5036-46EA-A4EA-345476C2F678}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -148,6 +152,14 @@ Global
{07054AA0-E8D5-4CD2-A774-A515CB4F2EBD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{07054AA0-E8D5-4CD2-A774-A515CB4F2EBD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{07054AA0-E8D5-4CD2-A774-A515CB4F2EBD}.Release|Any CPU.Build.0 = Release|Any CPU
{6DCEC050-2B9B-4DF7-AE21-D554965EF5A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6DCEC050-2B9B-4DF7-AE21-D554965EF5A1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6DCEC050-2B9B-4DF7-AE21-D554965EF5A1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6DCEC050-2B9B-4DF7-AE21-D554965EF5A1}.Release|Any CPU.Build.0 = Release|Any CPU
{BE9FFEDE-5036-46EA-A4EA-345476C2F678}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BE9FFEDE-5036-46EA-A4EA-345476C2F678}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BE9FFEDE-5036-46EA-A4EA-345476C2F678}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BE9FFEDE-5036-46EA-A4EA-345476C2F678}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -161,6 +173,8 @@ Global
{20AEA7CD-4388-4F1B-ADA5-B547CE69709A} = {27F9F1A6-C6B4-4539-B013-8549B4A6E1B5}
{C6935C85-DA7D-47B2-934B-2E90568EB4EC} = {D05F59F8-14B6-44AB-8FE6-493EE0CA4A75}
{5336DEEE-6183-44BE-9A9F-DAF2F85C9465} = {D05F59F8-14B6-44AB-8FE6-493EE0CA4A75}
{6DCEC050-2B9B-4DF7-AE21-D554965EF5A1} = {27F9F1A6-C6B4-4539-B013-8549B4A6E1B5}
{BE9FFEDE-5036-46EA-A4EA-345476C2F678} = {27F9F1A6-C6B4-4539-B013-8549B4A6E1B5}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {96DAE6A9-B1CC-4FF7-B08C-D5FBFD55B385}

View File

@@ -39,7 +39,6 @@ let render = React.functionComponent(fun () ->
.configureLogging(LogLevel.Debug)
.onMessage <|
function
| Response.Howdy -> JS.console.log("Howdy!")
| Response.NewCount i -> setCount i
| Response.RandomCharacter str -> setText str
)
@@ -60,7 +59,6 @@ module SignalRHub =
printfn "New Msg: %A" msg
match msg with
| Action.SayHello -> Response.Howdy
| Action.IncrementCount i -> Response.NewCount(i + 1)
| Action.DecrementCount i -> Response.NewCount(i - 1)
| Action.RandomCharacter ->
@@ -91,15 +89,12 @@ type Action =
| IncrementCount of int
| DecrementCount of int
| RandomCharacter
| SayHello
[<RequireQualifiedAccess>]
type Response =
| Howdy
| NewCount of int
| RandomCharacter of string
module Endpoints =
let [<Literal>] Root = "/SignalR"
```

View File

@@ -1,2 +1,2 @@
### 0.0.1 - Wednesday, March 25, 2020
* Initial build
### 0.1.0 - Monday, June 29th, 2020
* Initial release

View File

@@ -24,7 +24,6 @@ module App =
| IncrementCount
| DecrementCount
| RandomCharacter
| SayHello
| RegisterHub of Elmish.Hub<Action,Response>
let init =
@@ -42,7 +41,6 @@ module App =
| RegisterHub hub -> { model with Hub = Some hub }, Cmd.none
| SignalRMsg rsp ->
match rsp with
| Response.Howdy -> model, Cmd.none
| Response.RandomCharacter str ->
{ model with Text = str }, Cmd.none
| Response.NewCount i ->
@@ -53,8 +51,6 @@ module App =
model, Cmd.SignalR.send model.Hub (Action.DecrementCount model.Count)
| RandomCharacter ->
model, Cmd.SignalR.send model.Hub Action.RandomCharacter
| SayHello ->
model, Cmd.SignalR.send model.Hub Action.SayHello
let textDisplay = React.functionComponent(fun (input: {| count: int; text: string |}) ->
React.fragment [
@@ -103,7 +99,6 @@ module App =
| IncrementCount
| DecrementCount
| RandomCharacter
| SayHello
| RegisterHub of Elmish.Hub<Action,Response>
let init =
@@ -120,7 +115,6 @@ module App =
| RegisterHub hub -> { model with Hub = Some hub }, Cmd.none
| SignalRMsg rsp ->
match rsp with
| Response.Howdy -> model, Cmd.none
| Response.RandomCharacter str ->
{ model with Text = str }, Cmd.none
| Response.NewCount i ->
@@ -131,8 +125,6 @@ module App =
model, Cmd.SignalR.perform model.Hub (Action.DecrementCount model.Count) SignalRMsg
| RandomCharacter ->
model, Cmd.SignalR.perform model.Hub Action.RandomCharacter SignalRMsg
| SayHello ->
model, Cmd.SignalR.perform model.Hub Action.SayHello SignalRMsg
let textDisplay = React.functionComponent(fun (input: {| count: int; text: string |}) ->
React.fragment [
@@ -196,7 +188,6 @@ module App =
| IncrementCount
| DecrementCount
| RandomCharacter
| SayHello
| StartClientStream
| StartServerStream
| RegisterHub of Hub
@@ -223,7 +214,6 @@ module App =
| RegisterHub hub -> { model with Hub = Some hub }, Cmd.none
| SignalRMsg rsp ->
match rsp with
| Response.Howdy -> model, Cmd.none
| Response.RandomCharacter str ->
{ model with Text = str }, Cmd.none
| Response.NewCount i ->
@@ -236,8 +226,6 @@ module App =
model, Cmd.SignalR.send model.Hub (Action.DecrementCount model.Count)
| RandomCharacter ->
model, Cmd.SignalR.send model.Hub Action.RandomCharacter
| SayHello ->
model, Cmd.SignalR.send model.Hub Action.SayHello
| StartClientStream ->
let subject = SignalR.Subject<StreamTo.Action>()
@@ -360,7 +348,6 @@ module App =
.configureLogging(LogLevel.Debug)
.onMessage <|
function
| Response.Howdy -> JS.console.log("Howdy!")
| Response.NewCount i -> setCount i
| Response.RandomCharacter str -> setText str
)
@@ -426,7 +413,6 @@ module App =
hub.withUrl(Endpoints.Root)
.withAutomaticReconnect()
.configureLogging(LogLevel.Debug)
.onMessage <| fun (msg: Response) -> JS.console.log("")
)
Html.div [

View File

@@ -16,7 +16,7 @@ module App =
configure_signalr {
endpoint Endpoints.Root
send SignalRHub.send
invoke SignalRHub.update
invoke SignalRHub.invoke
stream_from SignalRHub.Stream.sendToClient
stream_to SignalRHub.Stream.getFromClient
with_log_level Microsoft.Extensions.Logging.LogLevel.None
@@ -25,14 +25,6 @@ module App =
logging (fun l -> l.AddFilter("Microsoft", LogLevel.Error) |> ignore)
error_handler (fun e log -> text e.Message)
url (sprintf "http://0.0.0.0:%i/" <| Env.getPortsOrDefault 8085us)
use_cors "Any" (fun policy ->
policy
.WithOrigins("localhost", "http://localhost:8080", "http://localhost", "http://127.0.0.1:80")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
|> ignore
)
no_router
use_static (Env.clientPath args)
use_developer_exceptions

View File

@@ -8,9 +8,8 @@ module SignalRHub =
open System.Collections.Generic
open FSharp.Control.Tasks.V2
let update (msg: Action) =
let invoke (msg: Action) =
match msg with
| Action.SayHello -> Response.Howdy
| Action.IncrementCount i -> Response.NewCount(i + 1)
| Action.DecrementCount i -> Response.NewCount(i - 1)
| Action.RandomCharacter ->
@@ -22,7 +21,7 @@ module SignalRHub =
|> Response.RandomCharacter
let send (msg: Action) (hubContext: FableHub<Action,Response>) =
update msg
invoke msg
|> hubContext.Clients.Caller.Send
[<RequireQualifiedAccess>]
@@ -30,9 +29,6 @@ module SignalRHub =
let sendToClient (msg: StreamFrom.Action) (hubContext: FableHub<Action,Response>) =
match msg with
| StreamFrom.Action.GenInts ->
Response.Howdy
|> hubContext.Clients.Caller.Send
|> Async.AwaitTask |> Async.Start
asyncSeq {
for i in [ 1 .. 100 ] do
yield StreamFrom.Response.GetInts i
@@ -41,5 +37,8 @@ module SignalRHub =
let getFromClient (clientStream: IAsyncEnumerable<StreamTo.Action>) (hubContext: FableHub<Action,Response>) =
AsyncSeq.ofAsyncEnum clientStream
|> AsyncSeq.iterAsync (fun _ -> async { return () })//(function | StreamTo.Action.GiveInt i -> hubContext.Clients.Caller.Send(Response.NewCount i) |> Async.AwaitTask)
|> AsyncSeq.iterAsync (function
| StreamTo.Action.GiveInt i ->
hubContext.Clients.Caller.Send(Response.NewCount i)
|> Async.AwaitTask)
|> Async.StartAsTask

View File

@@ -6,11 +6,9 @@ module SignalRHub =
| IncrementCount of int
| DecrementCount of int
| RandomCharacter
| SayHello
[<RequireQualifiedAccess>]
type Response =
| Howdy
| NewCount of int
| RandomCharacter of string
@@ -28,9 +26,5 @@ module SignalRHub =
type Action =
| GiveInt of int
module Endpoints =
let port = 8080us
let baseUrl = sprintf "http://localhost:%i" port
module Endpoints =
let [<Literal>] Root = "/SignalR"

View File

@@ -3,8 +3,6 @@
Any help is greatly appreciated!
Please ensure that all code matches the style of the rest of the api, and has comments to ensure a smooth IDE experience.
It is also very important that when possible to make the output code to be as "human" looking as possible so that troubleshooting
failing tests is as easy as possible.
I also ask that any new functionality added has matching tests to go with them so that we know it works as expected, as well as giving
examples for others to work from.

View File

@@ -64,7 +64,6 @@
{ title: "Sending Messages", link: "/signalr-client/messages" },
{ title: "Server Streaming", link: "/signalr-client/streaming-server" },
{ title: "Client Streaming", link: "/signalr-client/streaming-client" },
{ title: "Bidirectional Streaming", link: "/signalr-client/streaming-bidirectional" },
{ title: "API Reference", link: "/signalr-client/api" }
]
},
@@ -72,11 +71,16 @@
title: "Server Configuration",
links: [
{ title: "Introduction", link: "/signalr-server/" },
{ title: "AspNetCore/Giraffe", link: "/signalr-server/giraffe" },
{ title: "ASP.NET Core/Giraffe", link: "/signalr-server/aspnetcore" },
{ title: "Saturn", link: "/signalr-server/saturn" },
{ title: "API Reference", link: "/signalr-server/api" }
]
},
{
title: "Integration Testing",
link: "/integration-testing"
},
{
title: "Contributing",
link: "/contributing"

View File

@@ -11,6 +11,7 @@ dotnet add package Fable.SignalR
dotnet add package Fable.SignalR.Elmish // For Elmish Cmds
dotnet add package Fable.SignalR.Feliz // For Feliz hooks
# paket
paket add Fable.SignalR --project ./project/path
@@ -64,6 +65,7 @@ nuget packages into your F# project:
# nuget
dotnet add package Fable.SignalR.AspNetCore // For ASP.NET Core or Giraffe
dotnet add package Fable.SignalR.Saturn // For Saturn
# paket
paket add Fable.SignalR.AspNetCore --project ./project/path // For ASP.NET Core or Giraffe
paket add Fable.SignalR.Feliz --project ./project/path // For Saturn

View File

@@ -0,0 +1,57 @@
# Integration Testing
If you plan to run tests that use [jsdom](https://github.com/jsdom/jsdom),
such as with [Fable.Jester](https://github.com/Shmew/Fable.Jester/) there is
some configuration you will want to do so that your test environment can
properly connect to the server.
You can see a full example of how to do this in the [project repo](https://github.com/Shmew/Fable.SignalR/tree/master/tests).
## Create or modify the CORS policy
The jsdom environment will refuse your connection if this type of policy is
not in place:
```fsharp
application {
...
use_cors "Any" (fun policy ->
policy
.WithOrigins("http://localhost", "http://127.0.0.1:80")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
|> ignore
)
...
}
```
## Reduce logging (optional)
SignalR can at times be quite verbose, especially if you're running
your tests in the same console as the server.
### On the server
```fsharp
application {
use_signalr (
configure_signalr {
...
with_log_level Microsoft.Extensions.Logging.LogLevel.None
}
)
logging (fun l -> l.AddFilter("Microsoft", LogLevel.Error) |> ignore)
...
}
```
### On the client
```fsharp
let hub =
React.useSignalR<Action,Response>(fun hub ->
hub ...
.configureLogging(LogLevel.None)
```

View File

@@ -197,40 +197,9 @@ type IHubProtocol<'ClientStreamApi,'ServerApi,'ServerStreamApi> =
///
/// If IHubProtocol.transferFormat is 'Text', the result of method will be a string,
/// otherwise it will be an ArrayBuffer.
abstract writeMessage: message: Messages.HubMessage<'ClientStreamApi,'ServerApi,'ServerStreamApi>
-> U2<string, JS.ArrayBuffer>
```
## IHubProtocol
A protocol abstraction for communicating with SignalR Hubs.
Signature:
```fsharp
type IHubProtocol<'ClientStreamApi,'ServerApi,'ServerStreamApi> =
/// The name of the protocol. is used by SignalR to resolve the protocol between the client
/// and server.
abstract name: string
/// The version of the protocol.
abstract version: float
/// The TransferFormat of the protocol.
abstract transferFormat: TransferFormat
/// Creates an array of HubMessage objects from the specified serialized representation.
///
/// If IHubProtocol.transferFormat is 'Text', the `input` parameter must be a string, otherwise
/// it must be an ArrayBuffer.
abstract parseMessages : input: U3<string,JS.ArrayBuffer,Buffer> * ?logger: ILogger
-> ResizeArray<Messages.HubMessage<'ClientStreamApi,'ServerApi,'ServerStreamApi>>
/// Writes the specified HubMessage to a string or ArrayBuffer and returns it.
///
/// If IHubProtocol.transferFormat is 'Text', the result of method will be a string,
/// otherwise it will be an ArrayBuffer.
abstract writeMessage: message: Messages.HubMessage<'ClientStreamApi,'ServerApi,'ServerStreamApi>
-> U2<string, JS.ArrayBuffer>
abstract writeMessage:
message: Messages.HubMessage<'ClientStreamApi,'ServerApi,'ServerStreamApi>
-> U2<string, JS.ArrayBuffer>
```
## ISubject
@@ -284,11 +253,11 @@ type ConnectionState =
Signature:
```fsharp
type HubConnection<'ClientApi,'ClientStreamFromApi,'ClientStreamToApi,'ServerApi,'ServerStreamApi> =
/// Returns the base url of the hub connection.
member baseUrl () : string
/// The base url of the hub connection.
member baseUrl : string
/// Returns the connectionId to the hub of client.
member connectionId () : string option
/// The connectionId to the hub of client.
member connectionId : string option
/// Invokes a hub method on the server.
///
@@ -310,7 +279,7 @@ type HubConnection<'ClientApi,'ClientStreamFromApi,'ClientStreamToApi,'ServerApi
/// Allows the server to detect hard disconnects (like when a client unplugs their computer).
member keepAliveInterval : int
/// Removes all handlers for the specified hub method.
/// Removes all handlers.
member off () : unit
/// Registers a handler that will be invoked when the connection is closed.
@@ -339,7 +308,6 @@ type HubConnection<'ClientApi,'ClientStreamFromApi,'ClientStreamToApi,'ServerApi
/// Invokes a hub method on the server. Does not wait for a response from the receiver.
member sendNow (msg: 'ClientApi) : unit
member sendNow (msg: 'ClientApi, cancellationToken: System.Threading.CancellationToken) : unit
/// The server timeout in milliseconds.
///
@@ -357,7 +325,6 @@ type HubConnection<'ClientApi,'ClientStreamFromApi,'ClientStreamToApi,'ServerApi
/// Starts the connection immediately.
member startNow () : unit
member startNow (cancellationToken: System.Threading.CancellationToken) : unit
/// The state of the hub connection to the server.
member state : ConnectionState
@@ -372,8 +339,11 @@ type HubConnection<'ClientApi,'ClientStreamFromApi,'ClientStreamToApi,'ServerApi
member stopNow () : unit
/// Streams from the hub.
member streamFrom (msg: 'ClientStreamFromApi) : StreamResult<'ServerStreamApi>
member streamFrom (msg: 'ClientStreamFromApi) : Async<StreamResult<'ServerStreamApi>>
/// Streams from the hub.
member streamFromAsPromise (msg: 'ClientStreamFromApi) : Async<StreamResult<'ServerStreamApi>>
/// Returns an async that when invoked, starts streaming to the hub.
member streamTo (subject: ISubject<'ClientStreamToApi>) : Async<unit>
@@ -382,8 +352,6 @@ type HubConnection<'ClientApi,'ClientStreamFromApi,'ClientStreamToApi,'ServerApi
/// Streams to the hub immediately.
member streamToNow (subject: ISubject<'ClientStreamToApi>) : unit
member streamToNow (subject: ISubject<'ClientStreamToApi>,
cancellationToken: System.Threading.CancellationToken) : unit
```
## HubConnectionBuilder
@@ -482,6 +450,151 @@ static member NullLogger () : NullLogger
static member Subject<'T> () : Subject<'T>
```
## Elmish
All of the commands are in the `Cmd.SignalR` namespace.
### baseUrl
Returns the base url of the hub connection.
Signature:
```fsharp
(hub: #Elmish.Hub<'ClientApi,'ServerApi> option) (msg: string -> 'Msg) : Cmd<'Msg>
```
### connectionId
Returns the connectionId to the hub of this client.
Signature:
```fsharp
(hub: #Elmish.Hub<'ClientApi,'ServerApi> option) (msg: string option -> 'Msg) : Cmd<'Msg>
```
### connect
Starts a connection to a SignalR hub.
Signature:
```fsharp
(registerHub: Elmish.Hub<'ClientApi,'ServerApi> -> 'Msg)
(config: Elmish.HubConnectionBuilder<'ClientApi,unit,unit,'ServerApi,unit,'Msg>
-> Elmish.HubConnectionBuilder<'ClientApi,unit,unit,'ServerApi,unit,'Msg>) : Cmd<'Msg>
```
### attempt
Invokes a hub method on the server and maps the error.
This method resolves when the server indicates it has finished invoking the method. When it finishes,
the server has finished invoking the method. If the server method returns a result, it is produced as the result of
resolving the async call.
Signature:
```fsharp
(hub: #Elmish.Hub<'ClientApi,'ServerApi> option)
(msg: 'ClientApi)
(onError: exn -> 'Msg) : Cmd<'Msg>
```
### either
Invokes a hub method on the server and maps the success or error.
This method resolves when the server indicates it has finished invoking the method. When it finishes,
the server has finished invoking the method. If the server method returns a result, it is produced as the result of
resolving the async call.
Signature:
```fsharp
(hub: #Elmish.Hub<'ClientApi,'ServerApi> option)
(msg: 'ClientApi)
(onSuccess: 'ServerApi -> 'Msg)
(onError: exn -> 'Msg) : Cmd<'Msg>
```
### perform
Invokes a hub method on the server and maps the success.
This method resolves when the server indicates it has finished invoking the method. When it finishes,
the server has finished invoking the method. If the server method returns a result, it is produced as the result of
resolving the async call.
Signature:
```fsharp
(hub: #Elmish.Hub<'ClientApi,'ServerApi> option)
(msg: 'ClientApi)
(onSuccess: 'ServerApi -> 'Msg) : Cmd<'Msg>
```
### send
Invokes a hub method on the server. Does not wait for a response from the receiver.
This method resolves when the client has sent the invocation to the server. The server may still
be processing the invocation.
Signature:
```fsharp
(hub: #Elmish.Hub<'ClientApi,'ServerApi> option) (msg: 'ClientApi) : Cmd<'Msg>
```
### state
Returns the state of the Hub connection to the server.
Signature:
```fsharp
(hub: #Elmish.Hub<'ClientApi,'ServerApi> option) (msg: ConnectionState -> 'Msg) : Cmd<'Msg>
```
### streamFrom
Streams from the hub.
Signature:
```fsharp
(hub: Elmish.StreamHub.ServerToClient
<'ClientApi,'ClientStreamApi,'ServerApi,'ServerStreamApi> option)
(msg: 'ClientStreamApi)
(subscription: ISubscription -> 'Msg)
(subscriber: ('Msg -> unit) -> StreamSubscriber<'ServerStreamApi>) : Cmd<'Msg>
(hub: Elmish.StreamHub.Bidrectional
<'ClientApi,'ClientStreamFromApi,_,'ServerApi,'ServerStreamApi> option)
(msg: 'ClientStreamApi)
(subscription: ISubscription -> 'Msg)
(subscriber: ('Msg -> unit) -> StreamSubscriber<'ServerStreamApi>) : Cmd<'Msg>
```
### streamTo
Streams to the hub.
Signature:
```fsharp
(hub: Elmish.StreamHub.ClientToServer<'ClientApi,'ClientStreamToApi,'ServerApi> option)
(subject: #ISubject<'ClientStreamToApi>) : Cmd<'Msg>
(hub: Elmish.StreamHub.Bidrectional<'ClientApi,_,'ClientStreamToApi,'ServerApi,_> option)
(subject: #ISubject<'ClientStreamToApi>) : Cmd<'Msg>
```
## Feliz
The api exposed from the Feliz extension package is quite simple:
```fsharp
React.useSignalR<Types> (config: HubConnectionBuilder -> HubConnectionBuilder) -> Hub
```
The type of builder depends on the type restrictions given to `useSignalR`:
* No streaming - React.useSignalR<'ClientApi,'ServerApi>
* Client streaming - React.useSignalRReact.useSignalR<'ClientApi,'ClientStreamApi,'ServerApi>
* Server streaming - React.useSignalRReact.useSignalR<'ClientApi,'ClientStreamApi,'ServerApi,'ServerStreamApi>
* Bidirectional streaming - React.useSignalRReact.useSignalR<'ClientApi,'ClientStreamFromApi,'ClientStreamToApi,'ServerApi,'ServerStreamApi>
## Http
### XMLHttpRequestResponseType
@@ -537,7 +650,9 @@ type Request =
/// An AbortSignal that can be monitored for cancellation.
member abortSignal (signal: AbortSignal) : Request
/// The time to wait for the request to complete before throwing a TimeoutError. Measured in milliseconds.
/// The time to wait for the request to complete before throwing a TimeoutError.
///
/// Measured in milliseconds.
member timeout (value: int) : Request
/// controls whether credentials such as cookies are sent in cross-site requests.
@@ -569,21 +684,20 @@ type Client =
/// Issues an HTTP GET request to the specified URL, returning a Promise that
/// resolves with an HttpResponse representing the result.
abstract get: url: string -> JS.Promise<Response>
/// Issues an HTTP GET request to the specified URL, returning a Promise
/// that resolves with an HttpResponse representing the result.
abstract get: url: string * options: Request -> JS.Promise<Response>
/// Issues an HTTP POST request to the specified URL, returning a Promise that resolves with an HttpResponse representing the result.
/// Issues an HTTP POST request to the specified URL, returning a Promise
/// that resolves with an HttpResponse representing the result.
abstract post: url: string -> JS.Promise<Response>
/// Issues an HTTP POST request to the specified URL, returning a Promise that resolves with an HttpResponse representing the result.
abstract post: url: string * options: Request -> JS.Promise<Response>
/// Issues an HTTP DELETE request to the specified URL, returning a Promise that resolves with an HttpResponse representing the result.
/// Issues an HTTP DELETE request to the specified URL, returning a Promise
/// that resolves with an HttpResponse representing the result.
abstract delete: url: string -> JS.Promise<Response>
/// Issues an HTTP DELETE request to the specified URL, returning a Promise that resolves with an HttpResponse representing the result.
abstract delete: url: string * options: Request -> JS.Promise<Response>
/// Issues an HTTP request to the specified URL, returning a Promise that resolves with an HttpResponse representing the result.
/// Issues an HTTP request to the specified URL, returning a Promise
/// that resolves with an HttpResponse representing the result.
abstract send: request: Request -> JS.Promise<Response>
///Gets all cookies that apply to the specified URL.
@@ -597,7 +711,8 @@ Signature:
type DefaultClient =
inherit Client
/// Issues an HTTP request to the specified URL, returning a Promise that resolves with an HttpResponse representing the result.
/// Issues an HTTP request to the specified URL, returning a Promise
/// that resolves with an HttpResponse representing the result.
abstract send: request: Request -> JS.Promise<Response>
```
@@ -608,7 +723,8 @@ Configures the SignalR connection.
Signature:
```fsharp
type ConnectionBuilder =
/// Custom headers to be sent with every HTTP request. Note, setting headers in the browser will not work for WebSockets or the ServerSentEvents stream.
/// Custom headers to be sent with every HTTP request. Note, setting headers in
/// the browser will not work for WebSockets or the ServerSentEvents stream.
member header (headers: Map<string,string>) : ConnectionBuilder
/// An HttpClient that will be used to make HTTP requests.
@@ -619,15 +735,11 @@ type ConnectionBuilder =
/// Configures the logger used for logging.
///
/// Provide an ILogger instance, and log messages will be logged via that instance. Alternatively, provide a value from
/// the LogLevel enumeration and a default logger which logs to the Console will be configured to log messages of the specified
/// Provide an ILogger instance, and log messages will be logged via that instance.
/// Alternatively, provide a value from the LogLevel enumeration and a default
/// logger which logs to the Console will be configured to log messages of the specified
/// level (or higher).
member logger (logger: ILogger) : ConnectionBuilder
/// Configures the logger used for logging.
///
/// Provide an ILogger instance, and log messages will be logged via that instance. Alternatively, provide a value from
/// the LogLevel enumeration and a default logger which logs to the Console will be configured to log messages of the specified
/// level (or higher).
member logger (logLevel: LogLevel) : ConnectionBuilder
/// A function that provides an access token required for HTTP Bearer authentication.
@@ -642,13 +754,15 @@ type ConnectionBuilder =
/// A boolean indicating if negotiation should be skipped.
///
/// Negotiation can only be skipped when the IHttpConnectionOptions.transport property is set to 'HttpTransportType.WebSockets'.
/// Negotiation can only be skipped when the IHttpConnectionOptions.transport property
/// is set to 'HttpTransportType.WebSockets'.
member skipNegotiation (value: bool) : ConnectionBuilder
/// Default value is 'true'.
/// controls whether credentials such as cookies are sent in cross-site requests.
/// This controls whether credentials such as cookies are sent in cross-site requests.
///
/// Cookies are used by many load-balancers for sticky sessions which is required when your app is deployed with multiple servers.
/// Cookies are used by many load-balancers for sticky sessions which is required when
/// your app is deployed with multiple servers.
member withCredentials (value: bool) : ConnectionBuilder
```
@@ -845,14 +959,13 @@ type CancelInvocationMessage =
abstract invocationId: string
```
### CancelInvocationMessage
A hub message sent to request that a streaming invocation be canceled.
### HubMessage
Signature:
```fsharp
type HubMessage<'ClientStreamApi,'ServerApi,'ServerStreamApi> =
U7<InvocationMessage<'ServerApi>,
U8<InvocationMessage<'ServerApi>,
InvocationMessage<{| connectionId: string; message: 'ServerApi |}>,
StreamItemMessage<'ServerStreamApi>,
CompletionMessage<'ServerApi>,
StreamInvocationMessage<'ClientStreamApi>,

View File

@@ -193,48 +193,64 @@ let update msg model =
Sending messages is as simple as calling `invoke` from your hub:
```fsharp
let textDisplay = React.functionComponent(fun (input: {| count: int; text: string |}) ->
React.fragment [
Html.div input.count
Html.div input.text
])
let display = React.functionComponent(fun (input: {| hub: Hub<Action,Response> |}) ->
let count,setCount = React.useState 0
let text,setText = React.useState ""
let buttons = React.functionComponent(fun (input: {| count: int; hub: Hub<Action,Response> |}) ->
React.fragment [
Html.div [
Html.div count
Html.div text
]
Html.button [
prop.text "Increment"
prop.onClick <| fun _ -> input.hub.current.sendNow (Action.IncrementCount input.count)
prop.onClick <| fun _ ->
async {
let! rsp = input.hub.current.invoke (Action.IncrementCount count)
match rsp with
| Response.NewCount i -> setCount i
| _ -> ()
}
|> Async.StartImmediate
]
Html.button [
prop.text "Decrement"
prop.onClick <| fun _ -> input.hub.current.sendNow (Action.DecrementCount input.count)
prop.onClick <| fun _ ->
promise {
let! rsp = input.hub.current.invokeAsPromise (Action.DecrementCount count)
match rsp with
| Response.NewCount i -> setCount i
| _ -> ()
}
|> Promise.start
]
Html.button [
prop.text "Get Random Character"
prop.onClick <| fun _ -> input.hub.current.sendNow Action.RandomCharacter
prop.onClick <| fun _ ->
async {
let! rsp = input.hub.current.invoke Action.RandomCharacter
match rsp with
| Response.RandomCharacter str -> setText str
| _ -> ()
}
|> Async.StartImmediate
]
])
let render = React.functionComponent(fun () ->
let count,setCount = React.useState 0
let text,setText = React.useState ""
let hub =
React.useSignalR<Action,Response>(fun hub ->
hub.withUrl(Endpoints.Root)
.withAutomaticReconnect()
.configureLogging(LogLevel.Debug)
.onMessage <|
function
| Response.Howdy -> JS.console.log("Howdy!")
| Response.NewCount i -> setCount i
| Response.RandomCharacter str -> setText str
)
Html.div [
prop.children [
textDisplay {| count = count; text = text |}
buttons {| count = count; hub = hub |}
display {| hub = hub |}
]
])
```
@@ -242,4 +258,4 @@ let render = React.functionComponent(fun () ->
### Native
Same as the Feliz example, they both expose the same methods
for calling the SignalR hub.
for calling the SignalR hub.

View File

@@ -1,450 +0,0 @@
# Streaming
Data can also be streamed from the client and server without
having to repeatedly send new requests.
## Server Streaming
Streaming from the server allows you to call a request once
and get data sent to the provided subscriber.
Once you're ready to start a stream you will need to create a `StreamSubscriber<'T>`.
This is simply a record that defines how to handle responses for the three different cases
that can/will occur: `next`, `complete`, and `error`.
```fsharp
type StreamSubscriber<'T> =
{ /// Sends a new item to the server.
next: 'T -> unit
/// Sends an error to the server.
error: exn option -> unit
/// Completes the stream.
complete: unit -> unit }
```
If you're not using Elmish for state management, you can
instead pass a type that interfaces `IStreamSubscriber<'T>`.
```fsharp
type IStreamSubscriber<'T> =
/// Sends a new item to the server.
abstract next: value: 'T -> unit
/// Sends an error to the server.
abstract error: exn option -> unit
/// Completes the stream.
abstract complete: unit -> unit
```
This enables you to use other libraries that follow observer/subscriber patterns
with SignalR streaming.
### Elmish
To enable streaming in your Elmish model you will need to call
a different `connect` function. Instead of `Cmd.SignalR.connect` you
will call `Cmd.SignalR.Stream.ServerToClient.connect`.
This would look like:
```fsharp
type Model =
{ ...
Hub: Elmish.StreamHub.ServerToClient
<Action,StreamFrom.Action,Response,StreamFrom.Response> option
... }
let init () =
...
, Cmd.SignalR.Stream.ServerToClient.connect ...
```
The type definition can get pretty long, so creating a type alias can help
keep your code more concise.
Since you can't access the dispatch inside your update function directly, the Cmd takes a
function that given a dispatch returns your `StreamSubscriber<'T>`.
You then initiate a stream with the `Cmd.SignalR.streamFrom` command.
Putting it all together:
```fsharp
type Hub = Elmish.StreamHub.ServerToClient<Action,StreamFrom.Action,Response,StreamFrom.Response>
[<RequireQualifiedAccess>]
type StreamStatus =
| NotStarted
| Error of exn option
| Streaming
| Finished
type Model =
{ ...
Hub: Hub option
SFCount: int
StreamStatus: StreamStatus }
type Msg =
...
| SignalRStreamMsg of StreamFrom.Response
| StartServerStream
| StreamStatus of StreamStatus
let init =
{ ...
Hub = None
SFCount = 0
StreamSubscription = None
StreamStatus = StreamStatus.NotStarted }
, Cmd.SignalR.Stream.ServerToClient.connect RegisterHub (fun hub ->
hub.withUrl(Endpoints.Root)
.withAutomaticReconnect()
.configureLogging(LogLevel.Debug)
.onMessage SignalRMsg)
let update msg model =
match msg with
...
| StartServerStream ->
let subscriber dispatch =
{ next = SignalRStreamMsg >> dispatch
complete = fun () -> StreamStatus.Finished |> StreamStatus |> dispatch
error = StreamStatus.Error >> StreamStatus >> dispatch }
{ model with StreamStatus = StreamStatus.Streaming }
, Cmd.SignalR.streamFrom model.Hub StreamFrom.Action.GenInts Subscription subscriber
| StreamStatus ss -> { model with StreamStatus = ss }, Cmd.none
| SignalRStreamMsg (StreamFrom.Response.GetInts i) -> { model with SFCount = i }, Cmd.none
```
### Feliz
The first thing to note is that to enable streaming you must add additional typing
to your connection call:
Going from:
```fsharp
React.useSignalR<Action,Response>
```
To:
```fsharp
React.useSignalR<Action,StreamFrom.Action,Response,StreamFrom.Response>
```
Once you've done this you can initialize a server stream by calling the `streamFrom`
method on the Hub.
Putting it all together:
```fsharp
type Hub = StreamHub.ServerToClient<Action,StreamFrom.Action,Response,StreamFrom.Response>
let display = React.functionComponent(fun (input: {| hub: Hub |}) ->
let count,setCount = React.useState(0)
let subscriber =
{ next = fun (msg: StreamFrom.Response) ->
match msg with
| StreamFrom.Response.GetInts i ->
setCount(i)
complete = fun () -> JS.console.log("Complete!")
error = fun err -> JS.console.log(err) }
React.fragment [
Html.div count
Html.button [
prop.text "Stream From"
prop.onClick <| fun _ ->
let stream = input.hub.current.streamFrom StreamFrom.Action.GenInts
stream.subscribe(subscriber)
|> ignore
]
])
let render = React.functionComponent(fun () ->
let hub =
React.useSignalR<Action,StreamFrom.Action,Response,StreamFrom.Response>(fun hub ->
hub.withUrl(Endpoints.Root)
.withAutomaticReconnect()
.configureLogging(LogLevel.Debug)
.onMessage <| function | _ -> ()
)
Html.div [
prop.children [
display {| hub = hub |}
]
])
```
### Native
The first thing to note is that to enable streaming you must add additional typing
to your connection call:
Going from:
```fsharp
SignalR.connect<Action,_,_,Response,_>
```
To:
```fsharp
SignalR.connect<Action,StreamFrom.Action,_,Response,StreamFrom.Response>
```
Once you've done this you can initialize a server stream by calling the `streamFrom`
method on the Hub.
Putting it all together:
```fsharp
let subscriber =
{ next = fun (msg: StreamFrom.Response) ->
match msg with
| StreamFrom.Response.GetInts i ->
JS.console.log(i)
complete = fun () -> JS.console.log("Complete!")
error = fun err -> JS.console.log(err) }
let hub =
SignalR.connect<Action,StreamFrom.Action,unit,Response,StreamFrom.Response>(fun hub ->
hub.withUrl(Endpoints.Root)
.withAutomaticReconnect()
.configureLogging(LogLevel.Debug)
.onMessage <|
function
| Response.Howdy -> JS.console.log("Howdy!")
| Response.NewCount i -> JS.console.log(i)
| Response.RandomCharacter str -> JS.console.log(str))
hub.startNow()
let stream = hub.streamFrom StreamFrom.Action.GenInts
stream.subscribe(subscriber)
|> ignore
```
## Client Streaming
Streaming from the client allows you to send data to a subscriber on the server.
Once you're ready to start a stream you will need to create an `ISubject<'T>`.
```fsharp
type ISubject<'T> =
abstract next: item: 'T -> unit
abstract error: err: exn -> unit
abstract complete: unit -> unit
abstract subscribe: observer: #IStreamSubscriber<'T> -> ISubscription
```
Luckily you don't need to create your own implementation of a subject, as
the SignalR library has a native implementation. You can create this via
`SignalR.Subject<'T>()`. If you're using something besides Elmish for state
management you can implement the `ISubject<'T>` interface to use that (such as an
RxJS Subject).
### Elmish
To enable streaming in your Elmish model you will need to call
a different `connect` function. Instead of `Cmd.SignalR.connect` you
will call `Cmd.SignalR.Stream.ClientToServer.connect`.
This would look like:
```fsharp
type Model =
{ ...
Hub: Elmish.StreamHub.ClientToServer<Action,StreamTo.Action,Response> option
... }
let init () =
...
, Cmd.SignalR.Stream.ClientToServer.connect ...
```
You then initiate a stream with the `Cmd.SignalR.streamTo` command.
Putting it all together:
```fsharp
type Hub = Elmish.StreamHub.ClientToServer<Action,StreamFrom.Action,Response,StreamFrom.Response>
[<RequireQualifiedAccess>]
type StreamStatus =
| NotStarted
| Error of exn option
| Streaming
| Finished
type Model =
{ ...
Hub: Hub option
ClientStreamStatus: StreamStatus }
interface System.IDisposable with
member this.Dispose () =
this.Hub |> Option.iter (fun hub -> hub.Dispose())
this.StreamSubscription |> Option.iter (fun ss -> ss.dispose())
type Msg =
...
| StartClientStream
| Subscription of ISubscription
| StreamStatus of StreamStatus
let init =
{ ...
Hub = None
ClientStreamStatus = StreamStatus.NotStarted }
, Cmd.SignalR.Stream.ClientToServer.connect RegisterHub (fun hub ->
hub.withUrl(Endpoints.Root)
.withAutomaticReconnect()
.configureLogging(LogLevel.Debug)
.onMessage SignalRMsg)
let update msg model =
match msg with
...
| StartClientStream ->
let subject = SignalR.Subject<StreamTo.Action>()
model, Cmd.batch [
Cmd.SignalR.streamTo model.Hub subject
Cmd.ofSub (fun dispatch ->
let dispatch = ClientStreamStatus >> dispatch
dispatch StreamStatus.Streaming
async {
try
for i in [1..100] do
subject.next(StreamTo.Action.GiveInt i)
subject.complete()
dispatch StreamStatus.Finished
with e -> StreamStatus.Error(Some e) |> dispatch
}
|> Async.StartImmediate
)
]
| StreamStatus ss -> { model with StreamStatus = ss }, Cmd.none
| Subscription sub -> { model with StreamSubscription = Some sub }, Cmd.none
```
### Feliz
The first thing to note is that to enable streaming you must add additional typing
to your connection call:
Going from:
```fsharp
React.useSignalR<Action,Response>
```
To:
```fsharp
React.useSignalR<Action,StreamTo.Action.Action,Response>
```
Once you've done this you can initialize a server stream by calling the `streamTo`
method on the Hub.
Putting it all together:
```fsharp
type Hub = StreamHub.ClientToServer<Action,StreamTo.Action,Response>
let textDisplay = React.functionComponent(fun (input: {| count: int; text: string |}) ->
React.fragment [
Html.div input.count
Html.div input.text
])
let display = React.functionComponent(fun (input: {| count: int; hub: Hub |}) ->
Html.button [
prop.text "Stream To"
prop.onClick <| fun _ ->
async {
let subject = SignalR.Subject()
do! input.hub.current.streamTo(subject)
for i in [1..100] do
do! Async.Sleep 10
subject.next (StreamTo.Action.GiveInt i)
subject.complete()
}
|> Async.StartImmediate
])
let render = React.functionComponent(fun () ->
let count,setCount = React.useState 0
let text,setText = React.useState ""
let hub =
React.useSignalR<Action,StreamTo.Action,Response>(fun hub ->
hub.withUrl(Endpoints.Root)
.withAutomaticReconnect()
.configureLogging(LogLevel.Debug)
.onMessage <|
function
| Response.Howdy -> JS.console.log("Howdy!")
| Response.NewCount i -> setCount i
| Response.RandomCharacter str -> setText str
)
Html.div [
prop.children [
textDisplay {| count = count; text = text |}
display {| count = count; hub = hub |}
]
])
```
### Native
The first thing to note is that to enable streaming you must add additional typing
to your connection call:
Going from:
```fsharp
SignalR.connect<Action,_,_,Response,_>
```
To:
```fsharp
SignalR.connect<Action,_,StreamTo.Action,Response,_>
```
Once you've done this you can initialize a server stream by calling the `streamTo`
method on the Hub.
Putting it all together:
```fsharp
let hub =
SignalR.connect<Action,unit,StreamTo.Action,Response,unit>(fun hub ->
hub.withUrl(Endpoints.Root)
.withAutomaticReconnect()
.configureLogging(LogLevel.Debug)
.onMessage <|
function
| Response.Howdy -> JS.console.log("Howdy!")
| Response.NewCount i -> JS.console.log(i)
| Response.RandomCharacter str -> JS.console.log(str))
hub.startNow()
async {
let subject = SignalR.Subject()
do! hub.streamTo(subject)
for i in [1..100] do
subject.next (StreamTo.Action.GiveInt i)
subject.complete()
}
|> Async.StartImmediate
```

View File

@@ -150,9 +150,12 @@ let display = React.functionComponent(fun (input: {| hub: Hub |}) ->
Html.button [
prop.text "Stream From"
prop.onClick <| fun _ ->
let stream = input.hub.current.streamFrom StreamFrom.Action.GenInts
stream.subscribe(subscriber)
|> ignore
async {
let! stream = input.hub.current.streamFrom StreamFrom.Action.GenInts
stream.subscribe(subscriber)
|> ignore
}
|> Async.StartImmediate
]
])

View File

@@ -1,35 +1,15 @@
# Fable.Jester
# SignalR on the client
Fable.Jester are bindings to use [jest] and [jest-dom] to
test Fable applications.
Fable.SignalR.AspNetCore and Fable.SignalR.Saturn provide
a functional wrapper and enforced type-safety for SignalR hubs
with helpers to make configuration and usage easier.
The library has been developed to follow the native API as
closely as possible, while making some changes to allow for
better discoverablity.
## ASP.NET Core / Giraffe
Fable.Jester as a whole is exposed as two main types: `Jest`
and `expect`.
The AspNetCore extension library has no dependencies on Giraffe,
so it is compatible with native ASP.NET Core and Giraffe.
As long as you know of those two items you can find anything
that is available in this library. See those linked sections
for more details.
## Jest
## Saturn
The `Jest` type exposes almost every piece of functionality
in the library:
* [Describe blocks](/jest/describe)
* [Test blocks](/jest/test)
* [Global functions](/jest/globals)
* [Expect](/jest/expect)
## Expect
The [expect](/jest/expectHelpers) type is not the actual
method of making assertions, which can be confusing. The
purpose of this type is a collection of helper methods to
aid you when *actually* making your assertions.
[jest]: https://www.npmjs.com/package/jest
[jest-dom]: https://www.npmjs.com/package/@testing-library/jest-dom
The Saturn extension library adds computation expressions for
SignalR configuration as well as extends Saturn.

View File

@@ -1,190 +1,253 @@
# Expect
# API Reference
Jest exposes an `expect` object that is used to make
creating assertions easier.
One thing to note: `Fable.SignalR.Saturn` is a superset of `Fable.SignalR.AspNetCore`,
so only those designated as being Saturn-only are available with both libraries.
## addSnapshotSerializer
## FableHub
Add a module that formats application-specific data structures.
The `FableHub` is your interface for interacting with the SignalR hub.
Signature:
```fsharp
(serializer: obj) -> unit
type FableHub<'ClientApi,'ServerApi> =
abstract Clients : IHubCallerClients<IFableHubCallerClients<'ServerApi>>
abstract Context : HubCallerContext
abstract Groups : IGroupManager
abstract Invoke: 'ClientApi -> Task
abstract Send : 'ClientApi -> Task
abstract Dispose : unit -> unit
```
Usage:
```fsharp
expect.addSnapshotSerializer(import "serializer" "my-serializer-module")
```
## SignalR.Config
## any
Matches anything that was created with the given constructor.
Configuration options for customizing behavior of a SignalR hub.
Signature:
```fsharp
(value: 'Constructor)
type Config<'ClientApi,'ServerApi> =
{ /// Customize hub endpoint conventions.
EndpointConfig: (HubEndpointConventionBuilder -> HubEndpointConventionBuilder) option
/// Options used to configure hub instances.
HubOptions: (HubOptions -> unit) option
/// Adds a logging filter with the given LogLevel.
LogLevel: Microsoft.Extensions.Logging.LogLevel option
/// Called when a new connection is established with the hub.
OnConnected: (FableHub<'ClientApi,'ServerApi> -> Task<unit>) option
/// Called when a connection with the hub is terminated.
OnDisconnected: (exn -> FableHub<'ClientApi,'ServerApi> -> Task<unit>) option }
/// Creates an empty record.
static member Default () : Config<'ClientApi,'ServerApi>
```
Usage:
```fsharp
Jest.expect(myConstructedObj).toBe(expect.any(myConstructor)
```
## SignalR.Settings
## anything
Matches anything but null or undefined.
SignalR hub settings.
Signature:
```fsharp
unit
type Settings<'ClientApi,'ServerApi when 'ClientApi> =
{ /// The endpoint used to communicate with the hub.
EndpointPattern: string
/// Handler for client message sends.
Update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task
/// Handler for client invocations.
Invoke: 'ClientApi -> 'ServerApi
/// Optional hub configuration.
Config: Config<'ClientApi,'ServerApi> option }
```
Usage:
```fsharp
Jest.expect(1).toBe(expect.anything())
```
## SignalR.ConfigBuilder
## arrayContaining
Matches a received collection which contains all of the elements
in the expected array. That is, the expected collection is a
subset of the received collection. Therefore, it matches a
received collection which contains elements that are not in the
expected collection.
A fluent builder for the [config](#signalrconfig).
Signature:
```fsharp
(values: ResizeArray<'T>)
(values: 'T [])
(values: 'T list)
(values: 'T seq)
type ConfigBuilder<'ClientApi,'ServerApi> =
/// Customize hub endpoint conventions.
member EndpointConfig (f: HubEndpointConventionBuilder -> HubEndpointConventionBuilder)
: ConfigBuilder
/// Options used to configure hub instances.
member HubOptions (f: HubOptions -> unit) : ConfigBuilder
/// Adds a logging filter with the given LogLevel.
member LogLevel (logLevel: Microsoft.Extensions.Logging.LogLevel) : ConfigBuilder
/// Called when a new connection is established with the hub.
member OnConnected (f: FableHub<'ClientApi,'ServerApi> -> Task<unit>) : ConfigBuilder
/// Called when a connection with the hub is terminated.
member OnDisconnected (f: exn -> FableHub<'ClientApi,'ServerApi> -> Task<unit>) : ConfigBuilder
```
Usage:
## configure_signalr
<Note type="warning">Saturn only</Note>
Computation expression to build a configuration to feed into
the Saturn `use_signalr` operation.
Has the following operations:
```fsharp
let arraySample = [| 1;2;3;4;5;6;7 |]
configure_signalr {
/// The endpoint used to communicate with the hub.
endpoint: string
/// Handler for client message sends.
send: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> #Task
/// Handler for client invocations.
invoke: 'ClientApi -> 'ServerApi
/// Handler for streaming to the client.
stream_from: 'ClientStreamFromApi -> FableHub<'ClientApi,'ServerApi>
-> IAsyncEnumerable<'ServerStreamApi>
/// Handler for streaming from the client.
stream_to: IAsyncEnumerable<'ClientStreamToApi> -> FableHub<'ClientApi,'ServerApi> -> #Task
/// Customize hub endpoint conventions.
with_endpoint_config: HubEndpointConventionBuilder -> HubEndpointConventionBuilder
/// Options used to configure hub instances.
with_hub_options: HubOptions -> unit
/// Adds a logging filter with the given LogLevel.
with_log_level: Microsoft.Extensions.Logging.LogLevel
/// Called when a new connection is established with the hub.
with_on_connected: FableHub<'ClientApi,'ServerApi> -> Task<unit>
/// Called when a connection with the hub is terminated.
with_on_disconnected: exn -> FableHub<'ClientApi,'ServerApi> -> Task<unit>
}
Jest.expect(arraySample).toEqual(expect.arrayContaining([| 2;3;4 |]))
```
## assertions
# Type Extensions
Verifies that a certain number of assertions are called
during a test.
## IHostBuilder.SignalRLogLevel
Adds a logging filter for SignalR with the given log level threshold.
Signature:
```fsharp
(number: int) -> unit
(logLevel: Microsoft.Extensions.Logging.LogLevel) -> IHostBuilder
(settings: SignalR.Settings<'ClientApi,'ServerApi>) -> IHostBuilder
```
Usage:
```fsharp
expect.assertions(2)
```
## IServiceCollection.AddSignalR
## extend
Adds custom matchers to Jest.
See the [jest documentation](https://jestjs.io/docs/en/expect) for list
of `this` properties and methods
Adds SignalR services to the specified Microsoft.Extensions.DependencyInjection.IServiceCollection.
Signature:
```fsharp
(matchers: unit -> MatcherResponse) -> unit
(matchers: 'a -> MatcherResponse) -> unit
(matchers: 'a -> 'b -> MatcherResponse) -> unit
...
(settings: SignalR.Settings<'ClientApi,'ServerApi>) -> IServiceCollection
/// The response structure of matcher extensions.
type MatcherResponse =
abstract pass: bool
abstract message: unit -> string
(settings: SignalR.Settings<'ClientApi,'ServerApi>,
streamFrom: 'ClientStreamApi -> FableHub<'ClientApi,'ServerApi>
-> IAsyncEnumerable<'ServerStreamApi>) -> IServiceCollection
(settings: SignalR.Settings<'ClientApi,'ServerApi>,
streamTo: IAsyncEnumerable<'ClientStreamApi> -> FableHub<'ClientApi,'ServerApi> -> #Task)
-> IServiceCollection
(settings: SignalR.Settings<'ClientApi,'ServerApi>,
streamFrom: 'ClientStreamFromApi -> FableHub<'ClientApi,'ServerApi>
-> IAsyncEnumerable<'ServerStreamApi>,
streamTo: IAsyncEnumerable<'ClientStreamToApi> -> FableHub<'ClientApi,'ServerApi> -> #Task)
-> IServiceCollection
(endpoint: string,
update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> #Task,
invoke: 'ClientApi -> 'ServerApi)
-> IServiceCollection
(endpoint: string,
update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> #Task,
invoke: 'ClientApi -> 'ServerApi,
streamFrom: 'ClientStreamApi -> FableHub<'ClientApi,'ServerApi>
-> IAsyncEnumerable<'ServerStreamApi>) -> IServiceCollection
(endpoint: string,
update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> #Task,
invoke: 'ClientApi -> 'ServerApi,
streamTo: IAsyncEnumerable<'ClientStreamApi> -> FableHub<'ClientApi,'ServerApi> -> #Task)
-> IServiceCollection
(endpoint: string,
update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> #Task,
invoke: 'ClientApi -> 'ServerApi,
streamFrom: 'ClientStreamFromApi -> FableHub<'ClientApi,'ServerApi>
-> IAsyncEnumerable<'ServerStreamApi>,
streamTo: IAsyncEnumerable<'ClientStreamToApi> -> FableHub<'ClientApi,'ServerApi> -> #Task)
-> IServiceCollection
(endpoint: string,
update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> #Task,
invoke: 'ClientApi -> 'ServerApi,
config: SignalR.ConfigBuilder<'ClientApi,'ServerApi>
-> SignalR.ConfigBuilder<'ClientApi,'ServerApi>) -> IServiceCollection
(endpoint: string,
update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> #Task,
invoke: 'ClientApi -> 'ServerApi,
streamFrom: 'ClientStreamApi -> FableHub<'ClientApi,'ServerApi>
-> IAsyncEnumerable<'ServerStreamApi>,
config: SignalR.ConfigBuilder<'ClientApi,'ServerApi>
-> SignalR.ConfigBuilder<'ClientApi,'ServerApi>) -> IServiceCollection
(endpoint: string,
update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> #Task,
invoke: 'ClientApi -> 'ServerApi,
streamTo: IAsyncEnumerable<'ClientStreamApi> -> FableHub<'ClientApi,'ServerApi> -> #Task,
config: SignalR.ConfigBuilder<'ClientApi,'ServerApi>
-> SignalR.ConfigBuilder<'ClientApi,'ServerApi>) -> IServiceCollection
(endpoint: string,
update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> #Task,
invoke: 'ClientApi -> 'ServerApi,
streamFrom: 'ClientStreamFromApi -> FableHub<'ClientApi,'ServerApi>
-> IAsyncEnumerable<'ServerStreamApi>,
streamTo: IAsyncEnumerable<'ClientStreamToApi> -> FableHub<'ClientApi,'ServerApi> -> #Task,
config: SignalR.ConfigBuilder<'ClientApi,'ServerApi>
-> SignalR.ConfigBuilder<'ClientApi,'ServerApi>) -> IServiceCollection
```
Usage:
```fsharp
expect.extend(myExtension)
```
## IApplicationBuilder.UseSignalR
## hasAssertions
Verifies that at least one assertion is called during a test.
Adds SignalR services to the specified Microsoft.Extensions.DependencyInjection.IServiceCollection.
Signature:
```fsharp
unit -> unit
(settings: SignalR.Settings<'ClientApi,'ServerApi>) -> IApplicationBuilder
(settings: SignalR.Settings<'ClientApi,'ServerApi>,
streamFrom: 'ClientStreamApi -> FableHub<'ClientApi,'ServerApi>
-> IAsyncEnumerable<'ServerStreamApi>) -> IApplicationBuilder
(settings: SignalR.Settings<'ClientApi,'ServerApi>,
streamTo: IAsyncEnumerable<'ClientStreamApi> -> FableHub<'ClientApi,'ServerApi> -> #Task)
-> IApplicationBuilder
(settings: SignalR.Settings<'ClientApi,'ServerApi>,
streamFrom: 'ClientStreamFromApi -> FableHub<'ClientApi,'ServerApi>
-> IAsyncEnumerable<'ServerStreamApi>,
streamTo: IAsyncEnumerable<'ClientStreamToApi> -> FableHub<'ClientApi,'ServerApi> -> #Task)
-> IApplicationBuilder
```
Usage:
```fsharp
expect.hasAssertions()
```
## Application.ApplicationBuilder
## not
<Note type="warning">Saturn only</Note>
Inverts the pass/fail status of a matcher.
Usage:
```fsharp
Jest.expect("test").toEqual(expect.not.stringContaining("whoa"))
```
## objectContaining
Matches any received object that recursively matches the
expected properties. That is, the expected object is a
subset of the received object. Therefore, it matches a
received object which contains properties that are
present in the expected object.
Signature:
```fsharp
(value: obj)
```
Usage:
```fsharp
let actual =
{| someValue = "test"
someOtherValue = "testValue" |}
|> Fable.Core.JsInterop.toPlainJsObj
let expected =
{| someValue = "test" |}
|> Fable.Core.JsInterop.toPlainJsObj
Jest.expect(actual).toEqual(expect.objectContaining(expected))
```
## stringContaining
Matches the received value if it is a string that
contains the exact expected string.
Signature:
```fsharp
(value: string)
```
Usage:
```fsharp
Jest.expect("test").toEqual(expect.stringContaining("te"))
```
## stringMatching
Matches the received value if it is a string that matches
the expected string or regular expression.
Signature:
```fsharp
(value: string)
(value: Regex)
```
Usage:
```fsharp
Jest.expect("test").toEqual(expect.stringMatching(Regex("test")))
```
Extends the Saturn `application` computation expression to add the `use_signalr`
custom operation.

View File

@@ -0,0 +1,222 @@
# SignalR with ASP.NET Core and Giraffe
The use of ASP.NET Core and Giraffe is a bit
more involved than Saturn, but is only a couple extra
steps of configuration.
## Setting up a basic hub
To get started with the core functionality of SignalR
you only need to take a few steps.
### Define your domain
Firstly you will want to created a *shared* project that will
contain your shared message data structure.
For example:
```fsharp
namespace SignalRApp
module SignalRHub =
[<RequireQualifiedAccess>]
type Action =
| IncrementCount of int
| DecrementCount of int
[<RequireQualifiedAccess>]
type Response =
| NewCount of int
module Endpoints =
let [<Literal>] Root = "/SignalR"
```
### Handler functions
Once you have a shared model, it's fine to define how your
hub will behave.
There are two functions you will always need to provide when
creating a hub:
* invoke - A function that takes a client message (`Action`) and outputs the server response.
* send - A function that given a client message (`Action`) and hub context and (maybe) responds
(with a `Response`).
Following our example these would look like this:
```fsharp
module SignalRHub =
open Fable.SignalR
open SignalRHub
let invoke (msg: Action) =
match msg with
| Action.IncrementCount i -> Response.NewCount(i + 1)
| Action.DecrementCount i -> Response.NewCount(i - 1)
let send (msg: Action) (hubContext: FableHub<Action,Response>) =
invoke msg
|> hubContext.Clients.Caller.Send
```
### Adding it to the application
Now that you have a shared model and defined the behavior of your hub, all
that's left is to add it to the application.
It's easiest if you go ahead and define your configuration instead of inline it
in the fluent builders (but they support all of the overloads should you want to do so):
```fsharp
let mySignalRConfig =
{ EndpointPattern = Endpoints.Root
Send = SignalRHub.send
Invoke = SignalRHub.invoke
Config = None }
```
#### IServiceCollection
You will need to add SignalR to your `IServiceCollection`:
```fsharp
let myConfig serviceCollection =
serviceCollection.AddSignalR(mySignalRConfig)
```
#### IApplicationBuilder
Lastly you will want to also add it to the `IApplicationBuilder`:
```fsharp
let myApp appBuilder =
appBuilder.UseSignalR(mySignalRConfig)
```
That's it! You can now call your hub from the Fable client.
## Adding streaming
Similar to above, adding streaming is as easy as extending the steps we've
already done.
### Extend your domain
We need to add the new behavior in our shared model:
```fsharp
module SignalRHub =
[<RequireQualifiedAccess>]
type Action =
| IncrementCount of int
| DecrementCount of int
[<RequireQualifiedAccess>]
type Response =
| NewCount of int
// Streaming from the server
module StreamFrom =
[<RequireQualifiedAccess>]
type Action =
| GenInts
[<RequireQualifiedAccess>]
type Response =
| GetInts of int
// Streaming to the server
module StreamTo =
[<RequireQualifiedAccess>]
type Action =
| GiveInt of int
module Endpoints =
let [<Literal>] Root = "/SignalR"
```
### Add stream handler functions
If you want to support streaming either from the client and/or the
server you need to define the behavior you want:
* Streaming from - A function that takes a streaming message (`StreamFrom.Action`)
and hub context that then returns an `IAsyncEnumerable<StreamFrom.Response>`.
* Streaming to - A function that takes a `IAsyncEnumerable<StreamTo.Action>` and a hub context
and then (maybe) responds (with a `Response`).
Following our example the module would now look like this:
<Note type="tip">This is using the [FSharp.Control.AsyncSeq](https://github.com/fsprojects/FSharp.Control.AsyncSeq) library</Note>
```fsharp
module SignalRHub =
open Fable.SignalR
open FSharp.Control
open SignalRHub
open System.Collections.Generic
let invoke (msg: Action) =
match msg with
| Action.IncrementCount i -> Response.NewCount(i + 1)
| Action.DecrementCount i -> Response.NewCount(i - 1)
let send (msg: Action) (hubContext: FableHub<Action,Response>) =
invoke msg
|> hubContext.Clients.Caller.Send
[<RequireQualifiedAccess>]
module Stream =
let sendToClient (msg: StreamFrom.Action) (hubContext: FableHub<Action,Response>) =
match msg with
| StreamFrom.Action.GenInts ->
asyncSeq {
for i in [ 1 .. 100 ] do
yield StreamFrom.Response.GetInts i
}
|> AsyncSeq.toAsyncEnum
let getFromClient (clientStream: IAsyncEnumerable<StreamTo.Action>)
(hubContext: FableHub<Action,Response>) =
AsyncSeq.ofAsyncEnum clientStream
|> AsyncSeq.iterAsync (function
| StreamTo.Action.GiveInt i ->
hubContext.Clients.Caller.Send(Response.NewCount i)
|> Async.AwaitTask)
|> Async.StartAsTask
```
### Adding it to the application
Now that we've extended our model and defined our behavior we just
modify our configurations a bit and we're good to go!
#### IServiceCollection
Adjusting our `IServiceCollection` config to:
```fsharp
let myConfig serviceCollection =
serviceCollection.AddSignalR (
mySignalRConfig,
SignalRHub.Stream.sendToClient,
SignalRHub.Stream.getFromClient
)
```
#### IApplicationBuilder
Adjusting our `IApplicationBuilder` config to:
```fsharp
let myApp appBuilder =
appBuilder.UseSignalR (
mySignalRConfig,
SignalRHub.Stream.sendToClient,
SignalRHub.Stream.getFromClient
)
```
That's it! You can now call your hub from the Fable client.

View File

@@ -1,190 +0,0 @@
# Expect
Jest exposes an `expect` object that is used to make
creating assertions easier.
## addSnapshotSerializer
Add a module that formats application-specific data structures.
Signature:
```fsharp
(serializer: obj) -> unit
```
Usage:
```fsharp
expect.addSnapshotSerializer(import "serializer" "my-serializer-module")
```
## any
Matches anything that was created with the given constructor.
Signature:
```fsharp
(value: 'Constructor)
```
Usage:
```fsharp
Jest.expect(myConstructedObj).toBe(expect.any(myConstructor)
```
## anything
Matches anything but null or undefined.
Signature:
```fsharp
unit
```
Usage:
```fsharp
Jest.expect(1).toBe(expect.anything())
```
## arrayContaining
Matches a received collection which contains all of the elements
in the expected array. That is, the expected collection is a
subset of the received collection. Therefore, it matches a
received collection which contains elements that are not in the
expected collection.
Signature:
```fsharp
(values: ResizeArray<'T>)
(values: 'T [])
(values: 'T list)
(values: 'T seq)
```
Usage:
```fsharp
let arraySample = [| 1;2;3;4;5;6;7 |]
Jest.expect(arraySample).toEqual(expect.arrayContaining([| 2;3;4 |]))
```
## assertions
Verifies that a certain number of assertions are called
during a test.
Signature:
```fsharp
(number: int) -> unit
```
Usage:
```fsharp
expect.assertions(2)
```
## extend
Adds custom matchers to Jest.
See the [jest documentation](https://jestjs.io/docs/en/expect) for list
of `this` properties and methods
Signature:
```fsharp
(matchers: unit -> MatcherResponse) -> unit
(matchers: 'a -> MatcherResponse) -> unit
(matchers: 'a -> 'b -> MatcherResponse) -> unit
...
/// The response structure of matcher extensions.
type MatcherResponse =
abstract pass: bool
abstract message: unit -> string
```
Usage:
```fsharp
expect.extend(myExtension)
```
## hasAssertions
Verifies that at least one assertion is called during a test.
Signature:
```fsharp
unit -> unit
```
Usage:
```fsharp
expect.hasAssertions()
```
## not
Inverts the pass/fail status of a matcher.
Usage:
```fsharp
Jest.expect("test").toEqual(expect.not.stringContaining("whoa"))
```
## objectContaining
Matches any received object that recursively matches the
expected properties. That is, the expected object is a
subset of the received object. Therefore, it matches a
received object which contains properties that are
present in the expected object.
Signature:
```fsharp
(value: obj)
```
Usage:
```fsharp
let actual =
{| someValue = "test"
someOtherValue = "testValue" |}
|> Fable.Core.JsInterop.toPlainJsObj
let expected =
{| someValue = "test" |}
|> Fable.Core.JsInterop.toPlainJsObj
Jest.expect(actual).toEqual(expect.objectContaining(expected))
```
## stringContaining
Matches the received value if it is a string that
contains the exact expected string.
Signature:
```fsharp
(value: string)
```
Usage:
```fsharp
Jest.expect("test").toEqual(expect.stringContaining("te"))
```
## stringMatching
Matches the received value if it is a string that matches
the expected string or regular expression.
Signature:
```fsharp
(value: string)
(value: Regex)
```
Usage:
```fsharp
Jest.expect("test").toEqual(expect.stringMatching(Regex("test")))
```

View File

@@ -1,190 +1,194 @@
# Expect
# SignalR with Saturn
Jest exposes an `expect` object that is used to make
creating assertions easier.
The use of Saturn computation expressions makes
setting up a SignalR hub quite painless.
## addSnapshotSerializer
## Setting up a basic hub
Add a module that formats application-specific data structures.
To get started with the core functionality of SignalR
you only need to take a few steps.
### Define your domain
Firstly you will want to created a *shared* project that will
contain your shared message data structure.
For example:
Signature:
```fsharp
(serializer: obj) -> unit
namespace SignalRApp
module SignalRHub =
[<RequireQualifiedAccess>]
type Action =
| IncrementCount of int
| DecrementCount of int
[<RequireQualifiedAccess>]
type Response =
| NewCount of int
module Endpoints =
let [<Literal>] Root = "/SignalR"
```
Usage:
### Handler functions
Once you have a shared model, it's fine to define how your
hub will behave.
There are two functions you will always need to provide when
creating a hub:
* invoke - A function that takes a client message (`Action`) and outputs the server response.
* send - A function that given a client message (`Action`) and hub context and (maybe) responds
(with a `Response`).
Following our example these would look like this:
```fsharp
expect.addSnapshotSerializer(import "serializer" "my-serializer-module")
module SignalRHub =
open Fable.SignalR
open SignalRHub
let invoke (msg: Action) =
match msg with
| Action.IncrementCount i -> Response.NewCount(i + 1)
| Action.DecrementCount i -> Response.NewCount(i - 1)
let send (msg: Action) (hubContext: FableHub<Action,Response>) =
invoke msg
|> hubContext.Clients.Caller.Send
```
## any
### Adding it to the application
Matches anything that was created with the given constructor.
Now that you have a shared model and defined the behavior of your hub, all
that's left is to insert it into the Saturn pipeline.
<Note type="tip">For more information on the CE options see [here](api#configure_signalr)</Note>
Signature:
```fsharp
(value: 'Constructor)
application {
use_signalr (
configure_signalr {
endpoint Endpoints.Root
send SignalRHub.send
invoke SignalRHub.invoke
}
)
...
}
```
Usage:
That's it! You can now call your hub from the Fable client.
## Adding streaming
Similar to above, adding streaming is as easy as extending the steps we've
already done.
### Extend your domain
We need to add the new behavior in our shared model:
```fsharp
Jest.expect(myConstructedObj).toBe(expect.any(myConstructor)
module SignalRHub =
[<RequireQualifiedAccess>]
type Action =
| IncrementCount of int
| DecrementCount of int
[<RequireQualifiedAccess>]
type Response =
| NewCount of int
// Streaming from the server
module StreamFrom =
[<RequireQualifiedAccess>]
type Action =
| GenInts
[<RequireQualifiedAccess>]
type Response =
| GetInts of int
// Streaming to the server
module StreamTo =
[<RequireQualifiedAccess>]
type Action =
| GiveInt of int
module Endpoints =
let [<Literal>] Root = "/SignalR"
```
## anything
### Add stream handler functions
Matches anything but null or undefined.
If you want to support streaming either from the client and/or the
server you need to define the behavior you want:
* Streaming from - A function that takes a streaming message (`StreamFrom.Action`)
and hub context that then returns an `IAsyncEnumerable<StreamFrom.Response>`.
* Streaming to - A function that takes a `IAsyncEnumerable<StreamTo.Action>` and a hub context
and then (maybe) responds (with a `Response`).
Following our example the module would now look like this:
<Note type="tip">This is using the [FSharp.Control.AsyncSeq](https://github.com/fsprojects/FSharp.Control.AsyncSeq) library</Note>
Signature:
```fsharp
unit
module SignalRHub =
open Fable.SignalR
open FSharp.Control
open SignalRHub
open System.Collections.Generic
let invoke (msg: Action) =
match msg with
| Action.IncrementCount i -> Response.NewCount(i + 1)
| Action.DecrementCount i -> Response.NewCount(i - 1)
let send (msg: Action) (hubContext: FableHub<Action,Response>) =
invoke msg
|> hubContext.Clients.Caller.Send
[<RequireQualifiedAccess>]
module Stream =
let sendToClient (msg: StreamFrom.Action) (hubContext: FableHub<Action,Response>) =
match msg with
| StreamFrom.Action.GenInts ->
asyncSeq {
for i in [ 1 .. 100 ] do
yield StreamFrom.Response.GetInts i
}
|> AsyncSeq.toAsyncEnum
let getFromClient (clientStream: IAsyncEnumerable<StreamTo.Action>)
(hubContext: FableHub<Action,Response>) =
AsyncSeq.ofAsyncEnum clientStream
|> AsyncSeq.iterAsync (function
| StreamTo.Action.GiveInt i ->
hubContext.Clients.Caller.Send(Response.NewCount i)
|> Async.AwaitTask)
|> Async.StartAsTask
```
Usage:
### Adding it to the application
Now that we've extended our model and defined our behavior we just
add a couple new operations and we're good to go!
```fsharp
Jest.expect(1).toBe(expect.anything())
```
## arrayContaining
Matches a received collection which contains all of the elements
in the expected array. That is, the expected collection is a
subset of the received collection. Therefore, it matches a
received collection which contains elements that are not in the
expected collection.
Signature:
```fsharp
(values: ResizeArray<'T>)
(values: 'T [])
(values: 'T list)
(values: 'T seq)
```
Usage:
```fsharp
let arraySample = [| 1;2;3;4;5;6;7 |]
Jest.expect(arraySample).toEqual(expect.arrayContaining([| 2;3;4 |]))
```
## assertions
Verifies that a certain number of assertions are called
during a test.
Signature:
```fsharp
(number: int) -> unit
```
Usage:
```fsharp
expect.assertions(2)
```
## extend
Adds custom matchers to Jest.
See the [jest documentation](https://jestjs.io/docs/en/expect) for list
of `this` properties and methods
Signature:
```fsharp
(matchers: unit -> MatcherResponse) -> unit
(matchers: 'a -> MatcherResponse) -> unit
(matchers: 'a -> 'b -> MatcherResponse) -> unit
...
/// The response structure of matcher extensions.
type MatcherResponse =
abstract pass: bool
abstract message: unit -> string
```
Usage:
```fsharp
expect.extend(myExtension)
```
## hasAssertions
Verifies that at least one assertion is called during a test.
Signature:
```fsharp
unit -> unit
```
Usage:
```fsharp
expect.hasAssertions()
```
## not
Inverts the pass/fail status of a matcher.
Usage:
```fsharp
Jest.expect("test").toEqual(expect.not.stringContaining("whoa"))
```
## objectContaining
Matches any received object that recursively matches the
expected properties. That is, the expected object is a
subset of the received object. Therefore, it matches a
received object which contains properties that are
present in the expected object.
Signature:
```fsharp
(value: obj)
```
Usage:
```fsharp
let actual =
{| someValue = "test"
someOtherValue = "testValue" |}
|> Fable.Core.JsInterop.toPlainJsObj
let expected =
{| someValue = "test" |}
|> Fable.Core.JsInterop.toPlainJsObj
Jest.expect(actual).toEqual(expect.objectContaining(expected))
```
## stringContaining
Matches the received value if it is a string that
contains the exact expected string.
Signature:
```fsharp
(value: string)
```
Usage:
```fsharp
Jest.expect("test").toEqual(expect.stringContaining("te"))
```
## stringMatching
Matches the received value if it is a string that matches
the expected string or regular expression.
Signature:
```fsharp
(value: string)
(value: Regex)
```
Usage:
```fsharp
Jest.expect("test").toEqual(expect.stringMatching(Regex("test")))
application {
use_signalr (
configure_signalr {
endpoint Endpoints.Root
send SignalRHub.send
invoke SignalRHub.invoke
stream_from SignalRHub.Stream.sendToClient
stream_to SignalRHub.Stream.getFromClient
}
)
...
}
```

View File

@@ -14,20 +14,21 @@
},
"scripts": {
"build": "webpack -p",
"dev": "concurrently --kill-others \"yarn start-server-watch\" \"webpack-dev-server\"",
"pretest": "fable-splitter -c tests/Fable.SignalR.Tests/splitter.config.js",
"dev": "concurrently --kill-others \"yarn start-demo-server-watch\" \"webpack-dev-server\"",
"pretest": "rimraf ./dist/tests && fable-splitter -c tests/Fable.SignalR.Tests/splitter.config.js",
"publish-docs": "node publish.js",
"start": "live-server --port=8080 docs/",
"start-server": "npx ./startServer.js",
"start-server-watch": "npx nodemon -e fs,fsproj,fsi --watch ./demo/Server --watch src --watch tests --exec npx ./startServer.js",
"start-demo-server": "npx ./startDemoServer.js",
"start-test-server": "npx ./startTestServer.js",
"start-demo-server-watch": "npx nodemon -e fs,fsproj,fsi --watch src --watch demo --exec npx ./startDemoServer.js",
"start-test-server-watch": "npx nodemon -e fs,fsproj,fsi --watch src --watch tests --exec npx ./startTestServer.js",
"test": "jest",
"test-watch": "npx nodemon -e fs,fsproj,fsi --watch tests --watch src --watch demo --exec yarn test",
"test-server": "concurrently --success first --kill-others \"yarn start-server\" \"yarn test\"",
"test-server-watch": "concurrently --kill-others \"yarn start-server-watch\" \"yarn test-watch\""
"test-watch": "npx nodemon -e fs,fsproj,fsi --watch tests --watch src --exec yarn test",
"test-server": "concurrently --success first --kill-others \"yarn start-test-server\" \"yarn test\"",
"test-server-watch": "concurrently --kill-others \"yarn start-test-server-watch\" \"yarn test-watch\""
},
"dependencies": {
"@microsoft/signalr": "^3.1.4",
"nodemon": "^2.0.4",
"react": "^16",
"react-dom": "^16"
},
@@ -57,10 +58,12 @@
"live-server": "^1",
"mini-css-extract-plugin": "^0.9.0",
"node-sass": "^4",
"nodemon": "^2.0.4",
"npx": "^10.2.2",
"prettier": "^2",
"remotedev": "^0.2.9",
"resolve-url-loader": "^3",
"rimraf": "^3.0.2",
"sass": "^1",
"sass-loader": "^7",
"save": "^2",

View File

@@ -53,7 +53,7 @@ group Fable.SignalR.Server
nuget Fable.Remoting.Json ~> 2
nuget FSharp.Core ~> 4.7 lowest_matching:true
nuget Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson ~> 3
group Fable.SignalR.Tests
source https://nuget.org/api/v2
source https://api.nuget.org/v3/index.json
@@ -70,6 +70,21 @@ group Fable.SignalR.Tests
nuget Feliz.UseElmish ~> 1
nuget FSharp.Core ~> 4.7 lowest_matching:true
group Fable.SignalR.TestServer
source https://nuget.org/api/v2
source https://api.nuget.org/v3/index.json
nuget FSharp.Control.AsyncSeq ~> 2
nuget FSharp.Core ~> 4.7
nuget Saturn ~> 0.14.1
nuget TaskBuilder.fs 2.2.0-alpha
group Fable.SignalR.TestShared
source https://nuget.org/api/v2
source https://api.nuget.org/v3/index.json
nuget FSharp.Core ~> 4.7
group Client
source https://nuget.org/api/v2
source https://api.nuget.org/v3/index.json

File diff suppressed because one or more lines are too long

View File

@@ -14,9 +14,11 @@ module SignalRExtension =
open System.Threading.Tasks
type IHostBuilder with
/// Adds a logging filter for SignalR with the given log level threshold.
member this.SignalRLogLevel (logLevel: Microsoft.Extensions.Logging.LogLevel) =
this.ConfigureLogging(fun l -> l.AddFilter("Microsoft.AspNetCore.SignalR", logLevel) |> ignore)
/// Adds a logging filter for SignalR with the given log level threshold.
member this.SignalRLogLevel (settings: SignalR.Settings<'ClientApi,'ServerApi>) =
settings.Config
|> Option.bind(fun o -> o.LogLevel)
@@ -26,25 +28,25 @@ module SignalRExtension =
| None -> this
type IServiceCollection with
/// Adds SignalR services to the specified Microsoft.Extensions.DependencyInjection.IServiceCollection.
member this.AddSignalR (settings: SignalR.Settings<'ClientApi,'ServerApi>) =
let config =
let hubOptions = settings.Config |> Option.bind (fun s -> s.HubOptions)
match settings.Config with
| Some { OnConnected = Some onConnect; OnDisconnected = None } ->
{| Transient = FableHub.OnConnected.addTransient onConnect settings.Update settings.Invoke
{| Transient = FableHub.OnConnected.addTransient onConnect settings.Send settings.Invoke
HubOptions = hubOptions |}
| Some { OnConnected = None; OnDisconnected = Some onDisconnect } ->
{| Transient = FableHub.OnDisconnected.addTransient onDisconnect settings.Update settings.Invoke
{| Transient = FableHub.OnDisconnected.addTransient onDisconnect settings.Send settings.Invoke
HubOptions = hubOptions |}
| Some { OnConnected = Some onConnect; OnDisconnected = Some onDisconnect } ->
{| Transient = FableHub.Both.addTransient onConnect onDisconnect settings.Update settings.Invoke
{| Transient = FableHub.Both.addTransient onConnect onDisconnect settings.Send settings.Invoke
HubOptions = hubOptions |}
| _ ->
{| Transient = FableHub.addUpdateTransient settings.Update settings.Invoke
{| Transient = FableHub.addUpdateTransient settings.Send settings.Invoke
HubOptions = hubOptions |}
match config.HubOptions with
@@ -60,7 +62,8 @@ module SignalRExtension =
.AddSignalR()
.AddNewtonsoftJsonProtocol(fun o -> o.PayloadSerializerSettings.Converters.Add(FableJsonConverter()))
.Services |> config.Transient
/// Adds SignalR services to the specified Microsoft.Extensions.DependencyInjection.IServiceCollection.
member this.AddSignalR
(settings: SignalR.Settings<'ClientApi,'ServerApi>,
streamFrom: 'ClientStreamApi -> FableHub<'ClientApi,'ServerApi> -> IAsyncEnumerable<'ServerStreamApi>) =
@@ -70,19 +73,19 @@ module SignalRExtension =
match settings.Config with
| Some { OnConnected = Some onConnect; OnDisconnected = None } ->
{| Transient = FableHub.Stream.From.OnConnected.addTransient onConnect settings.Update settings.Invoke streamFrom
{| Transient = FableHub.Stream.From.OnConnected.addTransient onConnect settings.Send settings.Invoke streamFrom
HubOptions = hubOptions |}
| Some { OnConnected = None; OnDisconnected = Some onDisconnect } ->
{| Transient = FableHub.Stream.From.OnDisconnected.addTransient onDisconnect settings.Update settings.Invoke streamFrom
{| Transient = FableHub.Stream.From.OnDisconnected.addTransient onDisconnect settings.Send settings.Invoke streamFrom
HubOptions = hubOptions |}
| Some { OnConnected = Some onConnect; OnDisconnected = Some onDisconnect } ->
{| Transient = FableHub.Stream.From.Both.addTransient onConnect onDisconnect settings.Update settings.Invoke streamFrom
{| Transient = FableHub.Stream.From.Both.addTransient onConnect onDisconnect settings.Send settings.Invoke streamFrom
HubOptions = hubOptions |}
| _ ->
{| Transient =
{ Update = settings.Update
{ Send = settings.Send
Invoke = settings.Invoke
StreamFrom = streamFrom }
|> FableHub.Stream.From.addTransient
@@ -101,7 +104,8 @@ module SignalRExtension =
.AddSignalR()
.AddNewtonsoftJsonProtocol(fun o -> o.PayloadSerializerSettings.Converters.Add(FableJsonConverter()))
.Services |> config.Transient
/// Adds SignalR services to the specified Microsoft.Extensions.DependencyInjection.IServiceCollection.
member this.AddSignalR
(settings: SignalR.Settings<'ClientApi,'ServerApi>,
streamTo: IAsyncEnumerable<'ClientStreamApi> -> FableHub<'ClientApi,'ServerApi> -> #Task) =
@@ -111,19 +115,19 @@ module SignalRExtension =
match settings.Config with
| Some { OnConnected = Some onConnect; OnDisconnected = None } ->
{| Transient = FableHub.Stream.To.OnConnected.addTransient onConnect settings.Update settings.Invoke (Task.toGen streamTo)
{| Transient = FableHub.Stream.To.OnConnected.addTransient onConnect settings.Send settings.Invoke (Task.toGen streamTo)
HubOptions = hubOptions |}
| Some { OnConnected = None; OnDisconnected = Some onDisconnect } ->
{| Transient = FableHub.Stream.To.OnDisconnected.addTransient onDisconnect settings.Update settings.Invoke (Task.toGen streamTo)
{| Transient = FableHub.Stream.To.OnDisconnected.addTransient onDisconnect settings.Send settings.Invoke (Task.toGen streamTo)
HubOptions = hubOptions |}
| Some { OnConnected = Some onConnect; OnDisconnected = Some onDisconnect } ->
{| Transient = FableHub.Stream.To.Both.addTransient onConnect onDisconnect settings.Update settings.Invoke (Task.toGen streamTo)
{| Transient = FableHub.Stream.To.Both.addTransient onConnect onDisconnect settings.Send settings.Invoke (Task.toGen streamTo)
HubOptions = hubOptions |}
| _ ->
{| Transient =
{ Update = settings.Update
{ Send = settings.Send
Invoke = settings.Invoke
StreamTo = (Task.toGen streamTo) }
|> FableHub.Stream.To.addTransient
@@ -142,7 +146,8 @@ module SignalRExtension =
.AddSignalR()
.AddNewtonsoftJsonProtocol(fun o -> o.PayloadSerializerSettings.Converters.Add(FableJsonConverter()))
.Services |> config.Transient
/// Adds SignalR services to the specified Microsoft.Extensions.DependencyInjection.IServiceCollection.
member this.AddSignalR
(settings: SignalR.Settings<'ClientApi,'ServerApi>,
streamFrom: 'ClientStreamFromApi -> FableHub<'ClientApi,'ServerApi> -> IAsyncEnumerable<'ServerStreamApi>,
@@ -153,19 +158,19 @@ module SignalRExtension =
match settings.Config with
| Some { OnConnected = Some onConnect; OnDisconnected = None } ->
{| Transient = FableHub.Stream.Both.OnConnected.addTransient onConnect settings.Update settings.Invoke streamFrom (Task.toGen streamTo)
{| Transient = FableHub.Stream.Both.OnConnected.addTransient onConnect settings.Send settings.Invoke streamFrom (Task.toGen streamTo)
HubOptions = hubOptions |}
| Some { OnConnected = None; OnDisconnected = Some onDisconnect } ->
{| Transient = FableHub.Stream.Both.OnDisconnected.addTransient onDisconnect settings.Update settings.Invoke streamFrom (Task.toGen streamTo)
{| Transient = FableHub.Stream.Both.OnDisconnected.addTransient onDisconnect settings.Send settings.Invoke streamFrom (Task.toGen streamTo)
HubOptions = hubOptions |}
| Some { OnConnected = Some onConnect; OnDisconnected = Some onDisconnect } ->
{| Transient = FableHub.Stream.Both.Both.addTransient onConnect onDisconnect settings.Update settings.Invoke streamFrom (Task.toGen streamTo)
{| Transient = FableHub.Stream.Both.Both.addTransient onConnect onDisconnect settings.Send settings.Invoke streamFrom (Task.toGen streamTo)
HubOptions = hubOptions |}
| _ ->
{| Transient =
{ Update = settings.Update
{ Send = settings.Send
Invoke = settings.Invoke
StreamFrom = streamFrom
StreamTo = (Task.toGen streamTo) }
@@ -185,7 +190,8 @@ module SignalRExtension =
.AddSignalR()
.AddNewtonsoftJsonProtocol(fun o -> o.PayloadSerializerSettings.Converters.Add(FableJsonConverter()))
.Services |> config.Transient
/// Adds SignalR services to the specified Microsoft.Extensions.DependencyInjection.IServiceCollection.
member this.AddSignalR(endpoint: string, update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> #Task, invoke: 'ClientApi -> 'ServerApi) =
SignalR.ConfigBuilder(endpoint, Task.toGen update, invoke).Build()
|> this.AddSignalR
@@ -197,7 +203,8 @@ module SignalRExtension =
streamFrom: 'ClientStreamApi -> FableHub<'ClientApi,'ServerApi> -> IAsyncEnumerable<'ServerStreamApi>) =
this.AddSignalR(SignalR.ConfigBuilder(endpoint, Task.toGen update, invoke).Build(), streamFrom)
/// Adds SignalR services to the specified Microsoft.Extensions.DependencyInjection.IServiceCollection.
member this.AddSignalR
(endpoint: string,
update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> #Task,
@@ -205,7 +212,8 @@ module SignalRExtension =
streamTo: IAsyncEnumerable<'ClientStreamApi> -> FableHub<'ClientApi,'ServerApi> -> #Task) =
this.AddSignalR(SignalR.ConfigBuilder(endpoint, Task.toGen update, invoke).Build(), Task.toGen streamTo)
/// Adds SignalR services to the specified Microsoft.Extensions.DependencyInjection.IServiceCollection.
member this.AddSignalR
(endpoint: string,
update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> #Task,
@@ -214,7 +222,8 @@ module SignalRExtension =
streamTo: IAsyncEnumerable<'ClientStreamToApi> -> FableHub<'ClientApi,'ServerApi> -> #Task) =
this.AddSignalR(SignalR.ConfigBuilder(endpoint, Task.toGen update, invoke).Build(), streamFrom, Task.toGen streamTo)
/// Adds SignalR services to the specified Microsoft.Extensions.DependencyInjection.IServiceCollection.
member this.AddSignalR
(endpoint: string,
update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> #Task,
@@ -225,7 +234,8 @@ module SignalRExtension =
|> config
|> fun res -> res.Build()
|> this.AddSignalR
/// Adds SignalR services to the specified Microsoft.Extensions.DependencyInjection.IServiceCollection.
member this.AddSignalR
(endpoint: string,
update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> #Task,
@@ -236,7 +246,8 @@ module SignalRExtension =
SignalR.ConfigBuilder(endpoint, Task.toGen update, invoke)
|> config
|> fun res -> this.AddSignalR(res.Build(), streamFrom)
/// Adds SignalR services to the specified Microsoft.Extensions.DependencyInjection.IServiceCollection.
member this.AddSignalR
(endpoint: string,
update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> #Task,
@@ -247,7 +258,8 @@ module SignalRExtension =
SignalR.ConfigBuilder(endpoint, Task.toGen update, invoke)
|> config
|> fun res -> this.AddSignalR(res.Build(), Task.toGen streamTo)
/// Adds SignalR services to the specified Microsoft.Extensions.DependencyInjection.IServiceCollection.
member this.AddSignalR
(endpoint: string,
update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> #Task,
@@ -261,6 +273,7 @@ module SignalRExtension =
|> fun res -> this.AddSignalR(res.Build(), streamFrom, Task.toGen streamTo)
type IApplicationBuilder with
/// Configures routing and endpoints for the SignalR hub.
member this.UseSignalR (settings: SignalR.Settings<'ClientApi,'ServerApi>) =
let config =
@@ -286,7 +299,8 @@ module SignalRExtension =
.UseRouting()
// fsharplint:disable-next-line
.UseEndpoints(fun endpoints -> endpoints |> config |> ignore)
/// Configures routing and endpoints for the SignalR hub.
member this.UseSignalR
(settings: SignalR.Settings<'ClientApi,'ServerApi>,
streamFrom: 'ClientStreamApi -> FableHub<'ClientApi,'ServerApi> ->
@@ -315,7 +329,8 @@ module SignalRExtension =
.UseRouting()
// fsharplint:disable-next-line
.UseEndpoints(fun endpoints -> endpoints |> config |> ignore)
/// Configures routing and endpoints for the SignalR hub.
member this.UseSignalR
(settings: SignalR.Settings<'ClientApi,'ServerApi>,
streamTo: IAsyncEnumerable<'ClientStreamApi> -> FableHub<'ClientApi,'ServerApi> -> #Task) =
@@ -343,7 +358,8 @@ module SignalRExtension =
.UseRouting()
// fsharplint:disable-next-line
.UseEndpoints(fun endpoints -> endpoints |> config |> ignore)
/// Configures routing and endpoints for the SignalR hub.
member this.UseSignalR
(settings: SignalR.Settings<'ClientApi,'ServerApi>,
streamFrom: 'ClientStreamFromApi -> FableHub<'ClientApi,'ServerApi> ->

View File

@@ -22,7 +22,7 @@ type FableHub<'ClientApi,'ServerApi when 'ClientApi : not struct and 'ServerApi
[<EditorBrowsable(EditorBrowsableState.Never)>]
type NormalFableHubOptions<'ClientApi,'ServerApi when 'ClientApi : not struct and 'ServerApi : not struct> =
{ Update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task
{ Send: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task
Invoke: 'ClientApi -> 'ServerApi }
and [<EditorBrowsable(EditorBrowsableState.Never)>] NormalFableHub<'ClientApi,'ServerApi when 'ClientApi : not struct and 'ServerApi : not struct>
@@ -40,13 +40,13 @@ and [<EditorBrowsable(EditorBrowsableState.Never)>] NormalFableHub<'ClientApi,'S
member this.Invoke msg =
this.Clients.Caller.Invoke({| connectionId = this.Context.ConnectionId; message = settings.Invoke msg |})
member this.Send msg = settings.Update msg (this :> FableHub<'ClientApi,'ServerApi>)
member this.Send msg = settings.Send msg (this :> FableHub<'ClientApi,'ServerApi>)
[<EditorBrowsable(EditorBrowsableState.Never)>]
type StreamFromFableHubOptions<'ClientApi,'ClientStreamApi,'ServerApi,'ServerStreamApi
when 'ClientApi : not struct and 'ServerApi : not struct> =
{ Update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task
{ Send: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task
Invoke: 'ClientApi -> 'ServerApi
StreamFrom: 'ClientStreamApi -> FableHub<'ClientApi,'ServerApi> -> IAsyncEnumerable<'ServerStreamApi> }
@@ -65,13 +65,13 @@ and [<EditorBrowsable(EditorBrowsableState.Never)>] StreamFromFableHub<'ClientAp
member this.Dispose () = this.Dispose()
member this.Invoke msg = this.Clients.Caller.Invoke({| connectionId = this.Context.ConnectionId; message = settings.Invoke msg |})
member this.Send msg = settings.Update msg (this :> FableHub<'ClientApi,'ServerApi>)
member this.Send msg = settings.Send msg (this :> FableHub<'ClientApi,'ServerApi>)
member this.StreamFrom msg = settings.StreamFrom msg (this :> FableHub<'ClientApi,'ServerApi>)
type [<EditorBrowsable(EditorBrowsableState.Never)>] StreamToFableHubOptions<'ClientApi,'ClientStreamApi,'ServerApi
when 'ClientApi : not struct and 'ServerApi : not struct> =
{ Update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task
{ Send: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task
Invoke: 'ClientApi -> 'ServerApi
StreamTo: IAsyncEnumerable<'ClientStreamApi> -> FableHub<'ClientApi,'ServerApi> -> Task }
@@ -90,13 +90,13 @@ and [<EditorBrowsable(EditorBrowsableState.Never)>] StreamToFableHub<'ClientApi,
member this.Dispose () = this.Dispose()
member this.Invoke msg = this.Clients.Caller.Invoke({| connectionId = this.Context.ConnectionId; message = settings.Invoke msg |})
member this.Send msg = settings.Update msg (this :> FableHub<'ClientApi,'ServerApi>)
member this.Send msg = settings.Send msg (this :> FableHub<'ClientApi,'ServerApi>)
member this.StreamTo msg = settings.StreamTo msg (this :> FableHub<'ClientApi,'ServerApi>)
type [<EditorBrowsable(EditorBrowsableState.Never)>] StreamBothFableHubOptions<'ClientApi,'ClientStreamFromApi,'ClientStreamToApi,'ServerApi,'ServerStreamApi
when 'ClientApi : not struct and 'ServerApi : not struct> =
{ Update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task
{ Send: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task
Invoke: 'ClientApi -> 'ServerApi
StreamFrom: 'ClientStreamFromApi -> FableHub<'ClientApi,'ServerApi> -> IAsyncEnumerable<'ServerStreamApi>
StreamTo: IAsyncEnumerable<'ClientStreamToApi> -> FableHub<'ClientApi,'ServerApi> -> Task }
@@ -116,7 +116,7 @@ and [<EditorBrowsable(EditorBrowsableState.Never)>] StreamBothFableHub<'ClientAp
member this.Dispose () = this.Dispose()
member this.Invoke msg = this.Clients.Caller.Invoke({| connectionId = this.Context.ConnectionId; message = settings.Invoke msg |})
member this.Send msg = settings.Update msg (this :> FableHub<'ClientApi,'ServerApi>)
member this.Send msg = settings.Send msg (this :> FableHub<'ClientApi,'ServerApi>)
member this.StreamFrom msg = settings.StreamFrom msg (this :> FableHub<'ClientApi,'ServerApi>)
member this.StreamTo msg = settings.StreamTo msg (this :> FableHub<'ClientApi,'ServerApi>)
@@ -131,18 +131,18 @@ module FableHub =
type IOverride<'ClientApi,'ServerApi
when 'ClientApi : not struct and 'ServerApi : not struct> =
{ Update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task
{ Send: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task
Invoke: 'ClientApi -> 'ServerApi
OnConnected: FableHub<'ClientApi,'ServerApi> -> Task<unit> }
member this.AsNormalOptions : NormalFableHubOptions<'ClientApi,'ServerApi> =
{ Update = this.Update
{ Send = this.Send
Invoke = this.Invoke }
let addTransient onConnected update invoke (s: IServiceCollection) =
let addTransient onConnected send invoke (s: IServiceCollection) =
s.AddTransient<IOverride<'ClientApi,'ServerApi>> <|
System.Func<System.IServiceProvider,IOverride<'ClientApi,'ServerApi>>
(fun _ -> { Update = update; Invoke = invoke; OnConnected = onConnected })
(fun _ -> { Send = send; Invoke = invoke; OnConnected = onConnected })
type OnConnected<'ClientApi,'ServerApi
when 'ClientApi : not struct and 'ServerApi : not struct>
@@ -158,18 +158,18 @@ module FableHub =
type IOverride<'ClientApi,'ServerApi
when 'ClientApi : not struct and 'ServerApi : not struct> =
{ Update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task
{ Send: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task
Invoke: 'ClientApi -> 'ServerApi
OnDisconnected: exn -> FableHub<'ClientApi,'ServerApi> -> Task<unit> }
member this.AsNormalOptions : NormalFableHubOptions<'ClientApi,'ServerApi> =
{ Update = this.Update
{ Send = this.Send
Invoke = this.Invoke }
let addTransient onDisconnected update invoke (s: IServiceCollection) =
let addTransient onDisconnected send invoke (s: IServiceCollection) =
s.AddTransient<IOverride<'ClientApi,'ServerApi>> <|
System.Func<System.IServiceProvider,IOverride<'ClientApi,'ServerApi>>
(fun _ -> { Update = update; Invoke = invoke; OnDisconnected = onDisconnected })
(fun _ -> { Send = send; Invoke = invoke; OnDisconnected = onDisconnected })
type OnDisconnected<'ClientApi,'ServerApi
when 'ClientApi : not struct and 'ServerApi : not struct>
@@ -185,19 +185,19 @@ module FableHub =
type IOverride<'ClientApi,'ServerApi
when 'ClientApi : not struct and 'ServerApi : not struct> =
{ Update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task
{ Send: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task
Invoke: 'ClientApi -> 'ServerApi
OnConnected: FableHub<'ClientApi,'ServerApi> -> Task<unit>
OnDisconnected: exn -> FableHub<'ClientApi,'ServerApi> -> Task<unit> }
member this.AsNormalOptions : NormalFableHubOptions<'ClientApi,'ServerApi> =
{ Update = this.Update
{ Send = this.Send
Invoke = this.Invoke }
let addTransient onConnected onDisconnected update invoke (s: IServiceCollection) =
let addTransient onConnected onDisconnected send invoke (s: IServiceCollection) =
s.AddTransient<IOverride<'ClientApi,'ServerApi>> <|
System.Func<System.IServiceProvider,IOverride<'ClientApi,'ServerApi>>
(fun _ -> { Update = update; Invoke = invoke; OnConnected = onConnected; OnDisconnected = onDisconnected })
(fun _ -> { Send = send; Invoke = invoke; OnConnected = onConnected; OnDisconnected = onDisconnected })
type Both<'ClientApi,'ServerApi
when 'ClientApi : not struct and 'ServerApi : not struct>
@@ -219,23 +219,23 @@ module FableHub =
type IOverride<'ClientApi,'ClientStreamFromApi,'ClientStreamToApi,'ServerApi,'ServerStreamApi
when 'ClientApi : not struct and 'ServerApi : not struct> =
{ Update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task
{ Send: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task
Invoke: 'ClientApi -> 'ServerApi
StreamFrom: 'ClientStreamFromApi -> FableHub<'ClientApi,'ServerApi> -> IAsyncEnumerable<'ServerStreamApi>
StreamTo: IAsyncEnumerable<'ClientStreamToApi> -> FableHub<'ClientApi,'ServerApi> -> Task
OnConnected: FableHub<'ClientApi,'ServerApi> -> Task<unit> }
let addTransient onConnected update invoke streamFrom streamTo (s: IServiceCollection) =
let addTransient onConnected send invoke streamFrom streamTo (s: IServiceCollection) =
s.AddTransient<IOverride<'ClientApi,'ClientStreamFromApi,'ClientStreamToApi,'ServerApi,'ServerStreamApi>> <|
System.Func<System.IServiceProvider,IOverride<'ClientApi,'ClientStreamFromApi,'ClientStreamToApi,'ServerApi,'ServerStreamApi>>
(fun _ -> { Update = update; Invoke = invoke; StreamFrom = streamFrom; StreamTo = streamTo; OnConnected = onConnected })
(fun _ -> { Send = send; Invoke = invoke; StreamFrom = streamFrom; StreamTo = streamTo; OnConnected = onConnected })
type OnConnected<'ClientApi,'ClientStreamFromApi,'ClientStreamToApi,'ServerApi,'ServerStreamApi
when 'ClientApi : not struct and 'ServerApi : not struct>
(settings: OnConnected.IOverride<'ClientApi,'ClientStreamFromApi,'ClientStreamToApi,'ServerApi,'ServerStreamApi>) =
inherit StreamBothFableHub<'ClientApi,'ClientStreamFromApi,'ClientStreamToApi,'ServerApi,'ServerStreamApi>
({ Update = settings.Update; Invoke = settings.Invoke; StreamFrom = settings.StreamFrom; StreamTo = settings.StreamTo })
({ Send = settings.Send; Invoke = settings.Invoke; StreamFrom = settings.StreamFrom; StreamTo = settings.StreamTo })
override this.OnConnectedAsync () =
this :> FableHub<'ClientApi,'ServerApi>
@@ -245,23 +245,23 @@ module FableHub =
type IOverride<'ClientApi,'ClientStreamFromApi,'ClientStreamToApi,'ServerApi,'ServerStreamApi
when 'ClientApi : not struct and 'ServerApi : not struct> =
{ Update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task
{ Send: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task
Invoke: 'ClientApi -> 'ServerApi
StreamFrom: 'ClientStreamFromApi -> FableHub<'ClientApi,'ServerApi> -> IAsyncEnumerable<'ServerStreamApi>
StreamTo: IAsyncEnumerable<'ClientStreamToApi> -> FableHub<'ClientApi,'ServerApi> -> Task
OnDisconnected: exn -> FableHub<'ClientApi,'ServerApi> -> Task<unit> }
let addTransient onDisconnected update invoke streamFrom streamTo (s: IServiceCollection) =
let addTransient onDisconnected send invoke streamFrom streamTo (s: IServiceCollection) =
s.AddTransient<IOverride<'ClientApi,'ClientStreamFromApi,'ClientStreamToApi,'ServerApi,'ServerStreamApi>> <|
System.Func<System.IServiceProvider,IOverride<'ClientApi,'ClientStreamFromApi,'ClientStreamToApi,'ServerApi,'ServerStreamApi>>
(fun _ -> { Update = update; Invoke = invoke; StreamFrom = streamFrom; StreamTo = streamTo; OnDisconnected = onDisconnected })
(fun _ -> { Send = send; Invoke = invoke; StreamFrom = streamFrom; StreamTo = streamTo; OnDisconnected = onDisconnected })
type OnDisconnected<'ClientApi,'ClientStreamFromApi,'ClientStreamToApi,'ServerApi,'ServerStreamApi
when 'ClientApi : not struct and 'ServerApi : not struct>
(settings: OnDisconnected.IOverride<'ClientApi,'ClientStreamFromApi,'ClientStreamToApi,'ServerApi,'ServerStreamApi>) =
inherit StreamBothFableHub<'ClientApi,'ClientStreamFromApi,'ClientStreamToApi,'ServerApi,'ServerStreamApi>
({ Update = settings.Update; Invoke = settings.Invoke; StreamFrom = settings.StreamFrom; StreamTo = settings.StreamTo })
({ Send = settings.Send; Invoke = settings.Invoke; StreamFrom = settings.StreamFrom; StreamTo = settings.StreamTo })
override this.OnDisconnectedAsync (err: exn) =
this :> FableHub<'ClientApi,'ServerApi>
@@ -271,24 +271,24 @@ module FableHub =
type IOverride<'ClientApi,'ClientStreamFromApi,'ClientStreamToApi,'ServerApi,'ServerStreamApi
when 'ClientApi : not struct and 'ServerApi : not struct> =
{ Update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task
{ Send: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task
Invoke: 'ClientApi -> 'ServerApi
StreamFrom: 'ClientStreamFromApi -> FableHub<'ClientApi,'ServerApi> -> IAsyncEnumerable<'ServerStreamApi>
StreamTo: IAsyncEnumerable<'ClientStreamToApi> -> FableHub<'ClientApi,'ServerApi> -> Task
OnConnected: FableHub<'ClientApi,'ServerApi> -> Task<unit>
OnDisconnected: exn -> FableHub<'ClientApi,'ServerApi> -> Task<unit> }
let addTransient onConnected onDisconnected update invoke streamFrom streamTo (s: IServiceCollection) =
let addTransient onConnected onDisconnected send invoke streamFrom streamTo (s: IServiceCollection) =
s.AddTransient<IOverride<'ClientApi,'ClientStreamFromApi,'ClientStreamToApi,'ServerApi,'ServerStreamApi>> <|
System.Func<System.IServiceProvider,IOverride<'ClientApi,'ClientStreamFromApi,'ClientStreamToApi,'ServerApi,'ServerStreamApi>>
(fun _ -> { Update = update; Invoke = invoke; StreamFrom = streamFrom; StreamTo = streamTo; OnConnected = onConnected; OnDisconnected = onDisconnected })
(fun _ -> { Send = send; Invoke = invoke; StreamFrom = streamFrom; StreamTo = streamTo; OnConnected = onConnected; OnDisconnected = onDisconnected })
type Both<'ClientApi,'ClientStreamFromApi,'ClientStreamToApi,'ServerApi,'ServerStreamApi
when 'ClientApi : not struct and 'ServerApi : not struct>
internal (settings: Both.IOverride<'ClientApi,'ClientStreamFromApi,'ClientStreamToApi,'ServerApi,'ServerStreamApi>) =
inherit StreamBothFableHub<'ClientApi,'ClientStreamFromApi,'ClientStreamToApi,'ServerApi,'ServerStreamApi>
({ Update = settings.Update; Invoke = settings.Invoke; StreamFrom = settings.StreamFrom; StreamTo = settings.StreamTo })
({ Send = settings.Send; Invoke = settings.Invoke; StreamFrom = settings.StreamFrom; StreamTo = settings.StreamTo })
override this.OnConnectedAsync () =
this :> FableHub<'ClientApi,'ServerApi>
@@ -308,21 +308,21 @@ module FableHub =
type IOverride<'ClientApi,'ClientStreamApi,'ServerApi,'ServerStreamApi
when 'ClientApi : not struct and 'ServerApi : not struct> =
{ Update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task
{ Send: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task
Invoke: 'ClientApi -> 'ServerApi
Stream: 'ClientStreamApi -> FableHub<'ClientApi,'ServerApi> -> IAsyncEnumerable<'ServerStreamApi>
OnConnected: FableHub<'ClientApi,'ServerApi> -> Task<unit> }
let addTransient onConnected update invoke stream (s: IServiceCollection) =
let addTransient onConnected send invoke stream (s: IServiceCollection) =
s.AddTransient<IOverride<'ClientApi,'ClientStreamApi,'ServerApi,'ServerStreamApi>> <|
System.Func<System.IServiceProvider,IOverride<'ClientApi,'ClientStreamApi,'ServerApi,'ServerStreamApi>>
(fun _ -> { Update = update; Invoke = invoke; Stream = stream; OnConnected = onConnected })
(fun _ -> { Send = send; Invoke = invoke; Stream = stream; OnConnected = onConnected })
type OnConnected<'ClientApi,'ClientStreamApi,'ServerApi,'ServerStreamApi
when 'ClientApi : not struct and 'ServerApi : not struct>
(settings: OnConnected.IOverride<'ClientApi,'ClientStreamApi,'ServerApi,'ServerStreamApi>) =
inherit StreamFromFableHub<'ClientApi,'ClientStreamApi,'ServerApi,'ServerStreamApi>({ Update = settings.Update; Invoke = settings.Invoke; StreamFrom = settings.Stream })
inherit StreamFromFableHub<'ClientApi,'ClientStreamApi,'ServerApi,'ServerStreamApi>({ Send = settings.Send; Invoke = settings.Invoke; StreamFrom = settings.Stream })
override this.OnConnectedAsync () =
this :> FableHub<'ClientApi,'ServerApi>
@@ -332,21 +332,21 @@ module FableHub =
type IOverride<'ClientApi,'ClientStreamApi,'ServerApi,'ServerStreamApi
when 'ClientApi : not struct and 'ServerApi : not struct> =
{ Update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task
{ Send: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task
Invoke: 'ClientApi -> 'ServerApi
Stream: 'ClientStreamApi -> FableHub<'ClientApi,'ServerApi> -> IAsyncEnumerable<'ServerStreamApi>
OnDisconnected: exn -> FableHub<'ClientApi,'ServerApi> -> Task<unit> }
let addTransient onDisconnected update invoke stream (s: IServiceCollection) =
let addTransient onDisconnected send invoke stream (s: IServiceCollection) =
s.AddTransient<IOverride<'ClientApi,'ClientStreamApi,'ServerApi,'ServerStreamApi>> <|
System.Func<System.IServiceProvider,IOverride<'ClientApi,'ClientStreamApi,'ServerApi,'ServerStreamApi>>
(fun _ -> { Update = update; Invoke = invoke; Stream = stream; OnDisconnected = onDisconnected })
(fun _ -> { Send = send; Invoke = invoke; Stream = stream; OnDisconnected = onDisconnected })
type OnDisconnected<'ClientApi,'ClientStreamApi,'ServerApi,'ServerStreamApi
when 'ClientApi : not struct and 'ServerApi : not struct>
(settings: OnDisconnected.IOverride<'ClientApi,'ClientStreamApi,'ServerApi,'ServerStreamApi>) =
inherit StreamFromFableHub<'ClientApi,'ClientStreamApi,'ServerApi,'ServerStreamApi>({ Update = settings.Update; Invoke = settings.Invoke; StreamFrom = settings.Stream })
inherit StreamFromFableHub<'ClientApi,'ClientStreamApi,'ServerApi,'ServerStreamApi>({ Send = settings.Send; Invoke = settings.Invoke; StreamFrom = settings.Stream })
override this.OnDisconnectedAsync (err: exn) =
this :> FableHub<'ClientApi,'ServerApi>
@@ -356,22 +356,22 @@ module FableHub =
type IOverride<'ClientApi,'ClientStreamApi,'ServerApi,'ServerStreamApi
when 'ClientApi : not struct and 'ServerApi : not struct> =
{ Update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task
{ Send: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task
Invoke: 'ClientApi -> 'ServerApi
Stream: 'ClientStreamApi -> FableHub<'ClientApi,'ServerApi> -> IAsyncEnumerable<'ServerStreamApi>
OnConnected: FableHub<'ClientApi,'ServerApi> -> Task<unit>
OnDisconnected: exn -> FableHub<'ClientApi,'ServerApi> -> Task<unit> }
let addTransient onConnected onDisconnected update invoke stream (s: IServiceCollection) =
let addTransient onConnected onDisconnected send invoke stream (s: IServiceCollection) =
s.AddTransient<IOverride<'ClientApi,'ClientStreamApi,'ServerApi,'ServerStreamApi>> <|
System.Func<System.IServiceProvider,IOverride<'ClientApi,'ClientStreamApi,'ServerApi,'ServerStreamApi>>
(fun _ -> { Update = update; Stream = stream; Invoke = invoke; OnConnected = onConnected; OnDisconnected = onDisconnected })
(fun _ -> { Send = send; Stream = stream; Invoke = invoke; OnConnected = onConnected; OnDisconnected = onDisconnected })
type Both<'ClientApi,'ClientStreamApi,'ServerApi,'ServerStreamApi
when 'ClientApi : not struct and 'ServerApi : not struct>
(settings: Both.IOverride<'ClientApi,'ClientStreamApi,'ServerApi,'ServerStreamApi>) =
inherit StreamFromFableHub<'ClientApi,'ClientStreamApi,'ServerApi,'ServerStreamApi>({ Update = settings.Update; Invoke = settings.Invoke; StreamFrom = settings.Stream })
inherit StreamFromFableHub<'ClientApi,'ClientStreamApi,'ServerApi,'ServerStreamApi>({ Send = settings.Send; Invoke = settings.Invoke; StreamFrom = settings.Stream })
override this.OnConnectedAsync () =
this :> FableHub<'ClientApi,'ServerApi>
@@ -391,25 +391,25 @@ module FableHub =
type IOverride<'ClientApi,'ClientStreamApi,'ServerApi
when 'ClientApi : not struct and 'ServerApi : not struct> =
{ Update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task
{ Send: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task
Invoke: 'ClientApi -> 'ServerApi
Stream: IAsyncEnumerable<'ClientStreamApi> -> FableHub<'ClientApi,'ServerApi> -> Task
OnConnected: FableHub<'ClientApi,'ServerApi> -> Task<unit> }
member this.AsNormalOptions : NormalFableHubOptions<'ClientApi,'ServerApi> =
{ Update = this.Update
{ Send = this.Send
Invoke = this.Invoke }
let addTransient onConnected update invoke stream (s: IServiceCollection) =
let addTransient onConnected send invoke stream (s: IServiceCollection) =
s.AddTransient<IOverride<'ClientApi,'ClientStreamApi,'ServerApi>> <|
System.Func<System.IServiceProvider,IOverride<'ClientApi,'ClientStreamApi,'ServerApi>>
(fun _ -> { Update = update; Invoke = invoke; Stream = stream; OnConnected = onConnected })
(fun _ -> { Send = send; Invoke = invoke; Stream = stream; OnConnected = onConnected })
type OnConnected<'ClientApi,'ClientStreamApi,'ServerApi
when 'ClientApi : not struct and 'ServerApi : not struct>
(settings: OnConnected.IOverride<'ClientApi,'ClientStreamApi,'ServerApi>) =
inherit StreamToFableHub<'ClientApi,'ClientStreamApi,'ServerApi>({ Update = settings.Update; Invoke = settings.Invoke; StreamTo = settings.Stream })
inherit StreamToFableHub<'ClientApi,'ClientStreamApi,'ServerApi>({ Send = settings.Send; Invoke = settings.Invoke; StreamTo = settings.Stream })
override this.OnConnectedAsync () =
this :> FableHub<'ClientApi,'ServerApi>
@@ -419,21 +419,21 @@ module FableHub =
type IOverride<'ClientApi,'ClientStreamApi,'ServerApi
when 'ClientApi : not struct and 'ServerApi : not struct> =
{ Update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task
{ Send: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task
Invoke: 'ClientApi -> 'ServerApi
Stream: IAsyncEnumerable<'ClientStreamApi> -> FableHub<'ClientApi,'ServerApi> -> Task
OnDisconnected: exn -> FableHub<'ClientApi,'ServerApi> -> Task<unit> }
let addTransient onDisconnected update invoke stream (s: IServiceCollection) =
let addTransient onDisconnected send invoke stream (s: IServiceCollection) =
s.AddTransient<IOverride<'ClientApi,'ClientStreamApi,'ServerApi>> <|
System.Func<System.IServiceProvider,IOverride<'ClientApi,'ClientStreamApi,'ServerApi>>
(fun _ -> { Update = update; Invoke = invoke; Stream = stream; OnDisconnected = onDisconnected })
(fun _ -> { Send = send; Invoke = invoke; Stream = stream; OnDisconnected = onDisconnected })
type OnDisconnected<'ClientApi,'ClientStreamApi,'ServerApi
when 'ClientApi : not struct and 'ServerApi : not struct>
(settings: OnDisconnected.IOverride<'ClientApi,'ClientStreamApi,'ServerApi>) =
inherit StreamToFableHub<'ClientApi,'ClientStreamApi,'ServerApi>({ Update = settings.Update; Invoke = settings.Invoke; StreamTo = settings.Stream })
inherit StreamToFableHub<'ClientApi,'ClientStreamApi,'ServerApi>({ Send = settings.Send; Invoke = settings.Invoke; StreamTo = settings.Stream })
override this.OnDisconnectedAsync (err: exn) =
this :> FableHub<'ClientApi,'ServerApi>
@@ -443,22 +443,22 @@ module FableHub =
type IOverride<'ClientApi,'ClientStreamApi,'ServerApi
when 'ClientApi : not struct and 'ServerApi : not struct> =
{ Update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task
{ Send: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task
Invoke: 'ClientApi -> 'ServerApi
Stream: IAsyncEnumerable<'ClientStreamApi> -> FableHub<'ClientApi,'ServerApi> -> Task
OnConnected: FableHub<'ClientApi,'ServerApi> -> Task<unit>
OnDisconnected: exn -> FableHub<'ClientApi,'ServerApi> -> Task<unit> }
let addTransient onConnected onDisconnected update invoke stream (s: IServiceCollection) =
let addTransient onConnected onDisconnected send invoke stream (s: IServiceCollection) =
s.AddTransient<IOverride<'ClientApi,'ClientStreamApi,'ServerApi>> <|
System.Func<System.IServiceProvider,IOverride<'ClientApi,'ClientStreamApi,'ServerApi>>
(fun _ -> { Update = update; Invoke = invoke; Stream = stream; OnConnected = onConnected; OnDisconnected = onDisconnected })
(fun _ -> { Send = send; Invoke = invoke; Stream = stream; OnConnected = onConnected; OnDisconnected = onDisconnected })
type Both<'ClientApi,'ClientStreamApi,'ServerApi
when 'ClientApi : not struct and 'ServerApi : not struct>
(settings: Both.IOverride<'ClientApi,'ClientStreamApi,'ServerApi>) =
inherit StreamToFableHub<'ClientApi,'ClientStreamApi,'ServerApi>({ Update = settings.Update; Invoke = settings.Invoke; StreamTo = settings.Stream })
inherit StreamToFableHub<'ClientApi,'ClientStreamApi,'ServerApi>({ Send = settings.Send; Invoke = settings.Invoke; StreamTo = settings.Stream })
override this.OnConnectedAsync () =
this :> FableHub<'ClientApi,'ServerApi>
@@ -473,22 +473,28 @@ module FableHub =
System.Func<System.IServiceProvider,StreamToFableHubOptions<'ClientApi,'ClientStreamApi,'ServerApi>>
(fun _ -> settings)
let addUpdateTransient update invoke (s: IServiceCollection) =
let addUpdateTransient send invoke (s: IServiceCollection) =
s.AddTransient<NormalFableHubOptions<'ClientApi,'ServerApi>> <|
System.Func<System.IServiceProvider,NormalFableHubOptions<'ClientApi,'ServerApi>>
(fun _ -> { Update = update; Invoke = invoke })
(fun _ -> { Send = send; Invoke = invoke })
[<RequireQualifiedAccess>]
module SignalR =
/// Configuration options for customizing behavior of a SignalR hub.
[<RequireQualifiedAccess>]
type Config<'ClientApi,'ServerApi when 'ClientApi : not struct and 'ServerApi : not struct> =
{ EndpointConfig: (HubEndpointConventionBuilder -> HubEndpointConventionBuilder) option
{ /// Customize hub endpoint conventions.
EndpointConfig: (HubEndpointConventionBuilder -> HubEndpointConventionBuilder) option
/// Options used to configure hub instances.
HubOptions: (HubOptions -> unit) option
/// Adds a logging filter with the given LogLevel.
LogLevel: Microsoft.Extensions.Logging.LogLevel option
/// Called when a new connection is established with the hub.
OnConnected: (FableHub<'ClientApi,'ServerApi> -> Task<unit>) option
/// Called when a connection with the hub is terminated.
OnDisconnected: (exn -> FableHub<'ClientApi,'ServerApi> -> Task<unit>) option }
/// Creates an empty record.
static member Default () =
{ EndpointConfig = None
HubOptions = None
@@ -497,37 +503,44 @@ module SignalR =
OnDisconnected = None }
[<RequireQualifiedAccess>]
module Config =
module internal Config =
let bindEnpointConfig (settings: Config<'ClientApi,'ServerApi> option) (endpointBuilder: HubEndpointConventionBuilder) =
settings
|> Option.bind (fun c -> c.EndpointConfig |> Option.map (fun c -> c endpointBuilder))
|> Option.defaultValue endpointBuilder
/// SignalR hub settings.
type Settings<'ClientApi,'ServerApi when 'ClientApi : not struct and 'ServerApi : not struct> =
{ EndpointPattern: string
Update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task
{ /// The endpoint used to communicate with the hub.
EndpointPattern: string
/// Handler for client message sends.
Send: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task
/// Handler for client invocations.
Invoke: 'ClientApi -> 'ServerApi
/// Optional hub configuration.
Config: Config<'ClientApi,'ServerApi> option }
static member GetConfigOrDefault (settings: Settings<'ClientApi,'ServerApi>) =
static member internal GetConfigOrDefault (settings: Settings<'ClientApi,'ServerApi>) =
match settings.Config with
| None -> Config<'ClientApi,'ServerApi>.Default()
| Some config -> config
static member Create (endpointPattern: string, update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task, invoke: 'ClientApi -> 'ServerApi) =
static member internal Create (endpointPattern: string, update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task, invoke: 'ClientApi -> 'ServerApi) =
ConfigBuilder<'ClientApi,'ServerApi>(endpointPattern, update, invoke)
and ConfigBuilder<'ClientApi,'ServerApi when 'ClientApi : not struct and 'ServerApi : not struct>
internal
(endpoint: string,
update: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task,
send: 'ClientApi -> FableHub<'ClientApi,'ServerApi> -> Task,
invoke: 'ClientApi -> 'ServerApi) =
let mutable state =
{ EndpointPattern = endpoint
Update = update
Send = send
Invoke = invoke
Config = None }
/// Customize hub endpoint conventions.
member this.EndpointConfig (f: HubEndpointConventionBuilder -> HubEndpointConventionBuilder) =
state <-
{ state with
@@ -536,7 +549,8 @@ module SignalR =
EndpointConfig = Some f }
|> Some }
this
/// Options used to configure hub instances.
member this.HubOptions (f: HubOptions -> unit) =
state <-
{ state with
@@ -545,7 +559,8 @@ module SignalR =
HubOptions = Some f }
|> Some }
this
/// Adds a logging filter with the given LogLevel.
member this.LogLevel (logLevel: Microsoft.Extensions.Logging.LogLevel) =
state <-
{ state with
@@ -554,7 +569,8 @@ module SignalR =
LogLevel = Some logLevel }
|> Some }
this
/// Called when a new connection is established with the hub.
member this.OnConnected (f: FableHub<'ClientApi,'ServerApi> -> Task<unit>) =
state <-
{ state with
@@ -563,7 +579,8 @@ module SignalR =
OnConnected = Some f }
|> Some }
this
/// Called when a connection with the hub is terminated.
member this.OnDisconnected (f: exn -> FableHub<'ClientApi,'ServerApi> -> Task<unit>) =
state <-
{ state with
@@ -573,7 +590,7 @@ module SignalR =
|> Some }
this
member _.Build () = state
member internal _.Build () = state
[<assembly:System.Runtime.CompilerServices.InternalsVisibleTo("Fable.SignalR.Saturn")>]
do ()

View File

@@ -285,11 +285,11 @@ module Elmish =
|> dispatch ]
/// Returns the base url of the hub connection.
let baseUrl (hub: #Elmish.Hub<'ClientApi,'ServerApi> option) (msg: string -> 'Msg) : Cmd<_> =
let baseUrl (hub: #Elmish.Hub<'ClientApi,'ServerApi> option) (msg: string -> 'Msg) : Cmd<'Msg> =
[ fun dispatch -> hub |> Option.iter (fun hub -> hub.hub.baseUrl |> msg |> dispatch) ]
/// Returns the connectionId to the hub of this client.
let connectionId (hub: #Elmish.Hub<'ClientApi,'ServerApi> option) (msg: string option -> 'Msg) : Cmd<_> =
let connectionId (hub: #Elmish.Hub<'ClientApi,'ServerApi> option) (msg: string option -> 'Msg) : Cmd<'Msg> =
[ fun dispatch -> hub |> Option.iter (fun hub -> hub.hub.connectionId |> msg |> dispatch) ]
/// Starts a connection to a SignalR hub.
@@ -371,11 +371,11 @@ module Elmish =
///
/// This method resolves when the client has sent the invocation to the server. The server may still
/// be processing the invocation.
let send (hub: #Elmish.Hub<'ClientApi,'ServerApi> option) (msg: 'ClientApi) : Cmd<_> =
let send (hub: #Elmish.Hub<'ClientApi,'ServerApi> option) (msg: 'ClientApi) : Cmd<'Msg> =
[ fun _ -> hub |> Option.iter (fun hub -> hub.hub.sendNow msg) ]
/// Returns the state of the Hub connection to the server.
let state (hub: #Elmish.Hub<'ClientApi,'ServerApi> option) (msg: ConnectionState -> 'Msg) : Cmd<_> =
let state (hub: #Elmish.Hub<'ClientApi,'ServerApi> option) (msg: ConnectionState -> 'Msg) : Cmd<'Msg> =
[ fun dispatch -> hub |> Option.iter (fun hub -> hub.hub.state |> msg |> dispatch) ]
[<Erase>]

View File

@@ -40,10 +40,12 @@ module SignalRExtension =
member _.Yield(_) =
State.Empty.Init
/// The endpoint used to communicate with the hub.
[<CustomOperation("endpoint")>]
member _.Endpoint (State.Empty.Init, value: string) =
State.Endpoint.Value value
/// Handler for client message sends.
[<CustomOperation("send")>]
member _.Send (State.Endpoint.Value state, f: 'a -> FableHub<'a,'d> -> #Task) =
@@ -52,18 +54,20 @@ module SignalRExtension =
let send : State.Send<_,_> = State.Send.Value(state, f)
send
/// Handler for client invocations.
[<CustomOperation("invoke")>]
member _.Invoke (State.Send.Value (endpoint,send), f: 'a ->'b) =
let settings : SignalR.Settings<'a,'b> =
{ EndpointPattern = endpoint
Update = send
Send = send
Invoke = f
Config = None }
State.Settings.NoStream settings
/// Handler for streaming to the client.
[<CustomOperation("stream_from")>]
member _.StreamFrom (state: State.Settings<_,_,_,_,_>, f) =
@@ -72,7 +76,8 @@ module SignalRExtension =
| State.HasStreamFrom(settings,_) -> State.Settings.HasStreamFrom(settings, f)
| State.HasStreamTo(settings,streamTo) -> State.Settings.HasStreamBoth(settings, f, streamTo)
| State.NoStream(settings) -> State.Settings.HasStreamFrom(settings, f)
/// Handler for streaming from the client.
[<CustomOperation("stream_to")>]
member _.StreamTo(state: State.Settings<_,_,_,_,_>, f: IAsyncEnumerable<_> -> FableHub<_,_> -> #Task) =
@@ -83,7 +88,8 @@ module SignalRExtension =
| State.HasStreamFrom(settings,streamFrom) -> State.Settings.HasStreamBoth(settings, streamFrom, f)
| State.HasStreamTo(settings,_) -> State.Settings.HasStreamTo(settings, f)
| State.NoStream(settings) -> State.Settings.HasStreamTo(settings, f)
/// Customize hub endpoint conventions.
[<CustomOperation("with_endpoint_config")>]
member _.EndpointConfig (state: State.Settings<_,_,_,_,_>, f: HubEndpointConventionBuilder -> HubEndpointConventionBuilder) =
state.mapSettings <| fun state ->
@@ -92,16 +98,8 @@ module SignalRExtension =
{ SignalR.Settings.GetConfigOrDefault state with
EndpointConfig = Some f }
|> Some }
[<CustomOperation("with_log_level")>]
member _.LogLevel (state: State.Settings<_,_,_,_,_>, logLevel: Microsoft.Extensions.Logging.LogLevel) =
state.mapSettings <| fun state ->
{ state with
Config =
{ SignalR.Settings.GetConfigOrDefault state with
LogLevel = Some logLevel }
|> Some }
/// Options used to configure hub instances.
[<CustomOperation("with_hub_options")>]
member _.HubOptions (state: State.Settings<_,_,_,_,_>, f: HubOptions -> unit) =
state.mapSettings <| fun state ->
@@ -111,6 +109,17 @@ module SignalRExtension =
HubOptions = Some f }
|> Some }
/// Adds a logging filter with the given LogLevel.
[<CustomOperation("with_log_level")>]
member _.LogLevel (state: State.Settings<_,_,_,_,_>, logLevel: Microsoft.Extensions.Logging.LogLevel) =
state.mapSettings <| fun state ->
{ state with
Config =
{ SignalR.Settings.GetConfigOrDefault state with
LogLevel = Some logLevel }
|> Some }
/// Called when a new connection is established with the hub.
[<CustomOperation("with_on_connected")>]
member _.OnConnected (state: State.Settings<_,_,_,_,_>, f: FableHub<'ClientApi,'ServerApi> -> Task<unit>) =
state.mapSettings <| fun state ->
@@ -120,6 +129,7 @@ module SignalRExtension =
OnConnected = Some f }
|> Some }
/// Called when a connection with the hub is terminated.
[<CustomOperation("with_on_disconnected")>]
member _.OnDisconnected (state: State.Settings<_,_,_,_,_>, f: exn -> FableHub<'ClientApi,'ServerApi> -> Task<unit>) =
state.mapSettings <| fun state ->
@@ -138,10 +148,12 @@ module SignalRExtension =
[<AutoOpen>]
module Builder =
/// Creates a SignalR hub configuration.
// fsharplint:disable-next-line
let configure_signalr = SignalR.SettingsBuilder()
type Saturn.Application.ApplicationBuilder with
/// Adds a SignalR hub into the application.
[<CustomOperation("use_signalr")>]
member this.UseSignalR
(state, settings: SignalR.Settings<'ClientApi,'ServerApi> *

View File

@@ -79,7 +79,9 @@ module Http =
state <- { state with abortSignal = Some signal }
this
/// The time to wait for the request to complete before throwing a TimeoutError. Measured in milliseconds.
/// The time to wait for the request to complete before throwing a TimeoutError.
///
/// Measured in milliseconds.
member this.timeout (value: int) =
state <- { state with timeout = Some value }
this
@@ -109,17 +111,22 @@ module Http =
/// that resolves with an HttpResponse representing the result.
abstract get: url: string * options: Request -> JS.Promise<Response>
/// Issues an HTTP POST request to the specified URL, returning a Promise that resolves with an HttpResponse representing the result.
/// Issues an HTTP POST request to the specified URL, returning a Promise
/// that resolves with an HttpResponse representing the result.
abstract post: url: string -> JS.Promise<Response>
/// Issues an HTTP POST request to the specified URL, returning a Promise that resolves with an HttpResponse representing the result.
/// Issues an HTTP POST request to the specified URL, returning a Promise
/// that resolves with an HttpResponse representing the result.
abstract post: url: string * options: Request -> JS.Promise<Response>
/// Issues an HTTP DELETE request to the specified URL, returning a Promise that resolves with an HttpResponse representing the result.
/// Issues an HTTP DELETE request to the specified URL, returning a Promise
/// that resolves with an HttpResponse representing the result.
abstract delete: url: string -> JS.Promise<Response>
/// Issues an HTTP DELETE request to the specified URL, returning a Promise that resolves with an HttpResponse representing the result.
/// Issues an HTTP DELETE request to the specified URL, returning a Promise
/// that resolves with an HttpResponse representing the result.
abstract delete: url: string * options: Request -> JS.Promise<Response>
/// Issues an HTTP request to the specified URL, returning a Promise that resolves with an HttpResponse representing the result.
/// Issues an HTTP request to the specified URL, returning a Promise
/// that resolves with an HttpResponse representing the result.
abstract send: request: Request -> JS.Promise<Response>
///Gets all cookies that apply to the specified URL.
@@ -128,7 +135,8 @@ module Http =
type DefaultClient =
inherit Client
/// Issues an HTTP request to the specified URL, returning a Promise that resolves with an HttpResponse representing the result.
/// Issues an HTTP request to the specified URL, returning a Promise
/// that resolves with an HttpResponse representing the result.
abstract send: request: Request -> JS.Promise<Response>
type internal ConnectionOptions =
@@ -153,7 +161,8 @@ module Http =
skipNegotiation = None
withCredentials = None }
/// Custom headers to be sent with every HTTP request. Note, setting headers in the browser will not work for WebSockets or the ServerSentEvents stream.
/// Custom headers to be sent with every HTTP request. Note, setting headers in
/// the browser will not work for WebSockets or the ServerSentEvents stream.
member this.header (headers: Map<string,string>) =
state <- { state with headers = Some headers }
this
@@ -170,16 +179,18 @@ module Http =
/// Configures the logger used for logging.
///
/// Provide an ILogger instance, and log messages will be logged via that instance. Alternatively, provide a value from
/// the LogLevel enumeration and a default logger which logs to the Console will be configured to log messages of the specified
/// Provide an ILogger instance, and log messages will be logged via that instance.
/// Alternatively, provide a value from the LogLevel enumeration and a default
/// logger which logs to the Console will be configured to log messages of the specified
/// level (or higher).
member this.logger (logger: ILogger) =
state <- { state with logger = Some (U2.Case1(logger)) }
this
/// Configures the logger used for logging.
///
/// Provide an ILogger instance, and log messages will be logged via that instance. Alternatively, provide a value from
/// the LogLevel enumeration and a default logger which logs to the Console will be configured to log messages of the specified
/// Provide an ILogger instance, and log messages will be logged via that instance.
/// Alternatively, provide a value from the LogLevel enumeration and a default logger
/// which logs to the Console will be configured to log messages of the specified
/// level (or higher).
member this.logger (logLevel: LogLevel) =
state <- { state with logger = Some (U2.Case2(logLevel)) }
@@ -207,7 +218,8 @@ module Http =
/// A boolean indicating if negotiation should be skipped.
///
/// Negotiation can only be skipped when the IHttpConnectionOptions.transport property is set to 'HttpTransportType.WebSockets'.
/// Negotiation can only be skipped when the IHttpConnectionOptions.transport property
/// is set to 'HttpTransportType.WebSockets'.
member this.skipNegotiation (value: bool) =
state <- { state with skipNegotiation = Some value }
this
@@ -215,7 +227,8 @@ module Http =
/// Default value is 'true'.
/// This controls whether credentials such as cookies are sent in cross-site requests.
///
/// Cookies are used by many load-balancers for sticky sessions which is required when your app is deployed with multiple servers.
/// Cookies are used by many load-balancers for sticky sessions which is required when
/// your app is deployed with multiple servers.
member this.withCredentials (value: bool) =
state <- { state with withCredentials = Some value }
this

View File

@@ -326,10 +326,10 @@ module internal Bindings =
[<Erase>]
type Hub<'ClientApi,'ServerApi> =
/// Returns the base url of the hub connection.
/// The base url of the hub connection.
abstract baseUrl : string
/// Returns the connectionId to the hub of this client.
/// The connectionId to the hub of this client.
abstract connectionId : string option
/// Invokes a hub method on the server.
@@ -551,10 +551,10 @@ type HubConnection<'ClientApi,'ClientStreamFromApi,'ClientStreamToApi,'ServerApi
interface StreamHub.Bidrectional<'ClientApi,'ClientStreamFromApi,'ClientStreamToApi,'ServerApi,'ServerStreamApi>
/// Returns the base url of the hub connection.
/// The base url of the hub connection.
member _.baseUrl = hub.baseUrl
/// Returns the connectionId to the hub of this client.
/// The connectionId to the hub of this client.
member _.connectionId = hub.connectionId
/// Invokes a hub method on the server.
@@ -661,14 +661,13 @@ type HubConnection<'ClientApi,'ClientStreamFromApi,'ClientStreamToApi,'ServerApi
this.streamFrom(msg) |> Async.StartAsPromise
/// Returns an async that when invoked, starts streaming to the hub.
member _.streamTo (?subject: ISubject<'ClientStreamToApi>) =
let subject = Option.defaultValue (Bindings.signalR.Subject() :> ISubject<'ClientStreamToApi>) subject
member _.streamTo (subject: ISubject<'ClientStreamToApi>) =
async { return mailbox.Post(HubMailbox.Send(fun () -> hub.streamTo(subject))) }
/// Returns a promise that when invoked, starts streaming to the hub.
member this.streamToAsPromise (?subject: ISubject<'ClientStreamToApi>) =
this.streamTo(?subject = subject) |> Async.StartAsPromise
member this.streamToAsPromise (subject: ISubject<'ClientStreamToApi>) =
this.streamTo(subject) |> Async.StartAsPromise
/// Streams to the hub immediately.
member this.streamToNow (?subject: ISubject<'ClientStreamToApi>) =
this.streamTo(?subject = subject) |> fun a -> Async.StartImmediate(a, cts.Token)
member this.streamToNow (subject: ISubject<'ClientStreamToApi>) =
this.streamTo(subject) |> fun a -> Async.StartImmediate(a, cts.Token)

24
startTestServer.js Normal file
View File

@@ -0,0 +1,24 @@
const { spawn } = require('child_process')
const runServer = () => {
console.log("Starting server...")
const ps = spawn('dotnet', ['run', '-p', './tests/Fable.SignalR.TestServer/Fable.SignalR.TestServer.fsproj'])
ps.stdout.on('data', (data) => {
console.log(data.toString())
})
ps.stderr.on('data', (data) => {
console.error(data.toString())
})
process.on('exit', () => {
ps.kill()
})
process.on('SIGINT', () => ps.kill())
process.on('SIGTERM', () => ps.kill())
}
runServer()

View File

@@ -0,0 +1,48 @@
namespace SignalRApp
module App =
open Fable.SignalR
open Giraffe.ResponseWriters
open Microsoft.Extensions.Logging
open Saturn
open System
[<EntryPoint>]
let main args =
try
let app =
application {
use_signalr (
configure_signalr {
endpoint Endpoints.Root
send SignalRHub.send
invoke SignalRHub.invoke
stream_from SignalRHub.Stream.sendToClient
stream_to SignalRHub.Stream.getFromClient
with_log_level Microsoft.Extensions.Logging.LogLevel.None
}
)
logging (fun l -> l.AddFilter("Microsoft", LogLevel.Error) |> ignore)
error_handler (fun e log -> text e.Message)
url (sprintf "http://0.0.0.0:%i/" <| Env.getPortsOrDefault 8085us)
use_cors "Any" (fun policy ->
policy
.WithOrigins("http://localhost", "http://127.0.0.1:80")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
|> ignore
)
no_router
use_static (Env.clientPath args)
use_developer_exceptions
}
printfn "Working directory - %s" (System.IO.Directory.GetCurrentDirectory())
run app
0 // return an integer exit code
with e ->
let color = Console.ForegroundColor
Console.ForegroundColor <- System.ConsoleColor.Red
Console.WriteLine(e.Message)
Console.ForegroundColor <- color
1 // return an integer exit code

View File

@@ -0,0 +1,37 @@
namespace SignalRApp
module Env =
open System
open System.IO
let clientPath args =
match Array.toList args with
| clientPath :: _ when Directory.Exists clientPath -> clientPath
| _ ->
match (Path.Combine("..", "public")), (Path.Combine("..", "Client", "public")),
(Path.Combine("src", "Client", "public")) with
| path, _, _ when Directory.Exists path -> path
| _, path, _ when Directory.Exists path -> path
| _, _, path when Directory.Exists path -> path
| _ -> @"./public"
|> Path.GetFullPath
let getEnvFromAllOrNone (s: string) =
let envOpt (envVar: string) =
if envVar = "" || isNull envVar then None
else Some(envVar)
let procVar = Environment.GetEnvironmentVariable(s) |> envOpt
let userVar = Environment.GetEnvironmentVariable(s, EnvironmentVariableTarget.User) |> envOpt
let machVar = Environment.GetEnvironmentVariable(s, EnvironmentVariableTarget.Machine) |> envOpt
match procVar, userVar, machVar with
| Some(v), _, _
| _, Some(v), _
| _, _, Some(v) -> Some(v)
| _ -> None
let getPortsOrDefault defaultVal =
match getEnvFromAllOrNone "GIRAFFE_FABLE_PORT" with
| Some value -> value |> uint16
| None -> defaultVal

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
<ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
<ItemGroup>
<Compile Include="SignalR.fs" />
<Compile Include="Env.fs" />
<Compile Include="App.fs" />
<None Include="paket.references" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Fable.SignalR.Saturn\Fable.SignalR.Saturn.fsproj" />
<ProjectReference Include="..\Fable.SignalR.TestShared\Fable.SignalR.TestShared.fsproj" />
</ItemGroup>
<Import Project="..\..\.paket\Paket.Restore.targets" />
</Project>

View File

@@ -0,0 +1,43 @@
namespace SignalRApp
module SignalRHub =
open Fable.SignalR
open FSharp.Control
open SignalRHub
open System.Collections.Generic
let invoke (msg: Action) =
match msg with
| Action.SayHello -> Response.Howdy
| Action.IncrementCount i -> Response.NewCount(i + 1)
| Action.DecrementCount i -> Response.NewCount(i - 1)
| Action.RandomCharacter ->
let characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
System.Random().Next(0,characters.Length-1)
|> fun i -> characters.ToCharArray().[i]
|> string
|> Response.RandomCharacter
let send (msg: Action) (hubContext: FableHub<Action,Response>) =
invoke msg
|> hubContext.Clients.Caller.Send
[<RequireQualifiedAccess>]
module Stream =
let sendToClient (msg: StreamFrom.Action) (hubContext: FableHub<Action,Response>) =
match msg with
| StreamFrom.Action.GenInts ->
Response.Howdy
|> hubContext.Clients.Caller.Send
|> Async.AwaitTask |> Async.Start
asyncSeq {
for i in [ 1 .. 100 ] do
yield StreamFrom.Response.GetInts i
}
|> AsyncSeq.toAsyncEnum
let getFromClient (clientStream: IAsyncEnumerable<StreamTo.Action>) (hubContext: FableHub<Action,Response>) =
AsyncSeq.ofAsyncEnum clientStream
|> AsyncSeq.iterAsync (fun _ -> async { return () })//(function | StreamTo.Action.GiveInt i -> hubContext.Clients.Caller.Send(Response.NewCount i) |> Async.AwaitTask)
|> Async.StartAsTask

View File

@@ -0,0 +1,5 @@
group Fable.SignalR.TestServer
FSharp.Core
FSharp.Control.AsyncSeq
Saturn
TaskBuilder.fs

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp3.1</TargetFrameworks>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
</PropertyGroup>
<ItemGroup>
<Compile Include="Shared.fs" />
<None Include="paket.references" />
</ItemGroup>
<Import Project="..\..\.paket\Paket.Restore.targets" />
</Project>

View File

@@ -0,0 +1,32 @@
namespace SignalRApp
module SignalRHub =
[<RequireQualifiedAccess>]
type Action =
| IncrementCount of int
| DecrementCount of int
| RandomCharacter
| SayHello
[<RequireQualifiedAccess>]
type Response =
| Howdy
| NewCount of int
| RandomCharacter of string
module StreamFrom =
[<RequireQualifiedAccess>]
type Action =
| GenInts
[<RequireQualifiedAccess>]
type Response =
| GetInts of int
module StreamTo =
[<RequireQualifiedAccess>]
type Action =
| GiveInt of int
module Endpoints =
let [<Literal>] Root = "/SignalR"

View File

@@ -0,0 +1,2 @@
group Fable.SignalR.TestShared
FSharp.Core

View File

@@ -1,10 +1,7 @@
module Components
open Browser.Types
open Browser.Dom
open Elmish
open Fable.Core
open Fable.Core.JsInterop
open Fable.SignalR
open Fable.SignalR.Elmish
open Feliz

View File

@@ -12,10 +12,10 @@
<None Include="splitter.config.js" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\demo\Shared\Shared.fsproj" />
<ProjectReference Include="..\..\src\Fable.SignalR.Elmish\Fable.SignalR.Elmish.fsproj" />
<ProjectReference Include="..\..\src\Fable.SignalR.Feliz\Fable.SignalR.Feliz.fsproj" />
<ProjectReference Include="..\..\src\Fable.SignalR\Fable.SignalR.fsproj" />
<ProjectReference Include="..\Fable.SignalR.TestShared\Fable.SignalR.TestShared.fsproj" />
</ItemGroup>
<Import Project="..\..\.paket\Paket.Restore.targets" />
</Project>

View File

@@ -8765,7 +8765,7 @@ rimraf@2, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.
dependencies:
glob "^7.1.3"
rimraf@^3.0.0:
rimraf@^3.0.0, rimraf@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==