Finish docs
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
```
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
### 0.0.1 - Wednesday, March 25, 2020
|
||||
* Initial build
|
||||
### 0.1.0 - Monday, June 29th, 2020
|
||||
* Initial release
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
57
docs/integration-testing.md
Normal file
57
docs/integration-testing.md
Normal 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)
|
||||
```
|
||||
@@ -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>,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
]
|
||||
])
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
222
docs/signalr-server/aspnetcore.md
Normal file
222
docs/signalr-server/aspnetcore.md
Normal 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.
|
||||
@@ -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")))
|
||||
```
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
19
package.json
19
package.json
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
581
paket.lock
581
paket.lock
File diff suppressed because one or more lines are too long
@@ -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> ->
|
||||
|
||||
@@ -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 ()
|
||||
|
||||
@@ -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>]
|
||||
|
||||
@@ -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> *
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
24
startTestServer.js
Normal 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()
|
||||
48
tests/Fable.SignalR.TestServer/App.fs
Normal file
48
tests/Fable.SignalR.TestServer/App.fs
Normal 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
|
||||
37
tests/Fable.SignalR.TestServer/Env.fs
Normal file
37
tests/Fable.SignalR.TestServer/Env.fs
Normal 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
|
||||
@@ -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>
|
||||
43
tests/Fable.SignalR.TestServer/SignalR.fs
Normal file
43
tests/Fable.SignalR.TestServer/SignalR.fs
Normal 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
|
||||
5
tests/Fable.SignalR.TestServer/paket.references
Normal file
5
tests/Fable.SignalR.TestServer/paket.references
Normal file
@@ -0,0 +1,5 @@
|
||||
group Fable.SignalR.TestServer
|
||||
FSharp.Core
|
||||
FSharp.Control.AsyncSeq
|
||||
Saturn
|
||||
TaskBuilder.fs
|
||||
@@ -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>
|
||||
32
tests/Fable.SignalR.TestShared/Shared.fs
Normal file
32
tests/Fable.SignalR.TestShared/Shared.fs
Normal 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"
|
||||
2
tests/Fable.SignalR.TestShared/paket.references
Normal file
2
tests/Fable.SignalR.TestShared/paket.references
Normal file
@@ -0,0 +1,2 @@
|
||||
group Fable.SignalR.TestShared
|
||||
FSharp.Core
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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==
|
||||
|
||||
Reference in New Issue
Block a user