nhowka / elmish.bridge Goto Github PK
View Code? Open in Web Editor NEWCreate client-server Fable-Elmish apps keeping a single mindset
License: MIT License
Create client-server Fable-Elmish apps keeping a single mindset
License: MIT License
I tried broadcasting message using BroadcastServer
, but it is not being received on the server.
Looking at the code here
default sh.BroadcastServer(msg: 'inner) =
sh.SendClientIf (fun _ -> true) msg
shouldn't that be SendServerIf
instead?
default sh.BroadcastServer(msg: 'inner) =
sh.SendServerIf (fun _ -> true) msg
I'm using Elmish.Bridge.Server
version 6.0.0
(More like a discussion topic.)
Getting all the messages into the Shared isn't the best option for a big project.
The reason is too tight dependency on ClientMsg-s server side which for a UI reach project turns in necessity of including all pure UI messages (e.g. button pressed on say login screen) into the Shared project and making them visible on server. With corresponding models.
All this creates a bit of mess in the separation of concerns. Especially as UIs tend to grow in size quite fast comparing to the communication layer.
Really server shouldn't know about (and shouldn't depend on) pure UI things and on the UI side extracting server communication into a separate layer would be a bit cleaner as well.
So the proposal is to have in Shared something like this:
module WsBridge =
// Messages processed on the server
type ServerMsg =
| Closed
| ConnectUser of AuthToken
| DisconnectUser
//Messages processed on the client
type BridgeMsg =
| BS of BridgeServerMsg
| BC of BridgeClientMsg
and BridgeServerMsg =
| ConnectUserOnServer of AuthToken
| DisconnectUserOnServer
and BridgeClientMsg =
| ErrorResponse of ServerError * Request: ServerMsg
| ConnectionLost
| ServerConnected
| UserConnected of AuthToken
And in Client remapped BridgeMsg to ClientMsg with a separate pair of init()/update() functions (where AppMsg is visible in the Client project only and holds whole complex UI logic):
type ClientMsg =
| BridgeMsg of WsBridge.BridgeMsg
| AppMsg of AppMsg
type BridgedMsg = Msg<WsBridge.ServerMsg, ClientMsg>
I've seen your discussion with @Zaid-Ajaj and do support you in emphasising that Remoting and Bridge can coexist within the same project. For at least a couple of reasons:
I've got a working implementation but it's based on previous version of Remoting (the CE one - should be an issue so) and I'm not fully happy with one small arch decision (clearly need your advise on this one).
Will clean up the code and share this bit in a gist within the next couple of days.
Cheers
Yuriy
Hi @Nhowka,
Thank you for providing this great library to the community.
In a recent project I had to update the Client/Library.fs file to include a withWhenOpened function so that I could listen for WebSocket open events.
Is this a feature you think should be included in your library?
If so, would you like for me to submit a pull-request with this feature added?
Could you please provide an example of Bridge.withSubscription
?
As I understand, it accepts a function 'model -> Cmd<'a>
, like this:
Bridge.withSubscription (fun model -> Cmd.none)
But I'm puzzled how to emit command via this API.
What I need is to emit messages to server Bridge, from other asynchronous things, like MBP or Akka actors. Is withSubscription
is the right tool for this?
Typo in docs
Pay attention to the protocol and use ws:// or ws:// to connect.
I am thinking about using them, but it occurred to me that if I drop them there is no way the garbage collector will know that since it would be on a different machine. I was wondering if you've made sure to implement the finalizers so that they close out the side on the other machine? The connection being closed should also cause a disposal as well.
In this section
open Elmish
open Elmish.Bridge
let server =
Bridge.mkServer Shared.endpoint init update
|> Bridge.run Giraffe.server
let webApp = // <---- ??? not referenced
choose [
server // <---- referenced twice?
route "/" >=> htmlFile "/pages/index.html" ]
let app =
application {
router server // <---- ??? use_router? server referenced twice?
disable_diagnostics
app_config Giraffe.useWebSockets
url uri
}
run app
Should that read instead use_router webApp
?
There's a separate package for non-fable applications, that doesn't have any fable dependencies.
I receive the following error when I push a lot of messages in multiple threads. Any guidance what it could mean? It works fine for few background threads and then it dies
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
at Elmish.RingBuffer`1.Pop()
at [email protected](msg msg)
at [email protected](msg _arg1)
at Microsoft.FSharp.Control.AsyncPrimitives.CallThenInvokeNoHijackCheck[a,b](AsyncActivation`1 ctxt, b result1, FSharpFunc`2 userCode) in D:\a\_work\1\s\src\fsharp\FSharp.Core\async.fs:line 465
at Microsoft.FSharp.Control.Trampoline.Execute(FSharpFunc`2 firstAction) in D:\a\_work\1\s\src\fsharp\FSharp.Core\async.fs:line 104
--- End of stack trace from previous location ---
at [email protected](ExceptionDispatchInfo edi) in D:\a\_work\1\s\src\fsharp\FSharp.Core\async.fs:line 1078
at Microsoft.FSharp.Control.Trampoline.Execute(FSharpFunc`2 firstAction) in D:\a\_work\1\s\src\fsharp\FSharp.Core\async.fs:line 104
at <StartupCode$FSharp-Core>[email protected](Object o) in D:\a\_work\1\s\src\fsharp\FSharp.Core\async.fs:line 183
at System.Threading.ThreadPoolWorkQueue.Dispatch()
at System.Threading.PortableThreadPool.WorkerThread.WorkerThreadStart()
at System.Threading.Thread.StartCallback()
Given I am posting a message to the Hub with a type of List<'T>
from different threads. It looks like I'm getting the following error even if I made sure that the source object that I'm trying to send is not getting modified.
Stack trace shows something related to Array.Parallel
? I'm not entirely sure if it's due to the library?
Elmish.Bridge/src/Server/Library.fs
Line 196 in abc6df0
System.AggregateException: One or more errors occurred. (Collection was modified; enumeration operation may not execute.)
---> System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
at System.Collections.Generic.List`1.Enumerator.MoveNext()
at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeList(JsonWriter writer, IEnumerable values, JsonArrayContract contract, JsonProperty member, JsonContainerContract collectionContract, JsonProperty containerProperty)
at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeObject(JsonWriter writer, Object value, JsonObjectContract contract, JsonProperty member, JsonContainerContract collectionContract, JsonProperty containerProperty)
at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeList(JsonWriter writer, IEnumerable values, JsonArrayContract contract, JsonProperty member, JsonContainerContract collectionContract, JsonProperty containerProperty)
at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.Serialize(JsonWriter jsonWriter, Object value, Type objectType)
at Fable.Remoting.Json.FableJsonConverter.WriteJson(JsonWriter writer, Object value, JsonSerializer serializer)
at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeConvertable(JsonWriter writer, JsonConverter converter, Object value, JsonContract contract, JsonContainerContract collectionContract, JsonProperty containerProperty)
at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.Serialize(JsonWriter jsonWriter, Object value, Type objectType)
at Newtonsoft.Json.JsonSerializer.SerializeInternal(JsonWriter jsonWriter, Object value, Type objectType)
at Newtonsoft.Json.JsonConvert.SerializeObjectInternal(Object value, Type type, JsonSerializer jsonSerializer)
at <StartupCode$Elmish-Bridge-Server>[email protected](client x)
at Microsoft.FSharp.Collections.ArrayModule.Parallel.Iterate@1418.Invoke(Int32 i) in D:\a\_work\1\s\src\fsharp\FSharp.Core\array.fs:line 1418
at System.Threading.Tasks.Parallel.<>c__DisplayClass19_0`1.<ForWorker>b__1(RangeWorker& currentWorker, Int32 timeout, Boolean& replicationDelegateYieldedBeforeCompletion)
--- End of stack trace from previous location ---
at System.Threading.Tasks.Parallel.<>c__DisplayClass19_0`1.<ForWorker>b__1(RangeWorker& currentWorker, Int32 timeout, Boolean& replicationDelegateYieldedBeforeCompletion)
at System.Threading.Tasks.TaskReplicator.Replica.Execute()
--- End of inner exception stack trace ---
at System.Threading.Tasks.TaskReplicator.Run[TState](ReplicatableUserAction`1 action, ParallelOptions options, Boolean stopOnFirstFailure)
at System.Threading.Tasks.Parallel.ForWorker[TLocal](Int32 fromInclusive, Int32 toExclusive, ParallelOptions parallelOptions, Action`1 body, Action`2 bodyWithState, Func`4 bodyWithLocal, Func`1 localInit, Action`1 localFinally)
--- End of stack trace from previous location ---
at System.Threading.Tasks.Parallel.ThrowSingleCancellationExceptionOrOtherException(ICollection exceptions, CancellationToken cancelToken, Exception otherException)
at System.Threading.Tasks.Parallel.ForWorker[TLocal](Int32 fromInclusive, Int32 toExclusive, ParallelOptions parallelOptions, Action`1 body, Action`2 bodyWithState, Func`4 bodyWithLocal, Func`1 localInit, Action`1 localFinally)
at System.Threading.Tasks.Parallel.For(Int32 fromInclusive, Int32 toExclusive, Action`1 body)
at Microsoft.FSharp.Collections.ArrayModule.Parallel.Iterate[T](FSharpFunc`2 action, T[] array) in D:\a\_work\1\s\src\fsharp\FSharp.Core\array.fs:line 1417
at <StartupCode$Elmish-Bridge-Server>[email protected](Unit unitVar)
at Microsoft.FSharp.Control.AsyncPrimitives.CallThenInvoke[T,TResult](AsyncActivation`1 ctxt, TResult result1, FSharpFunc`2 part2) in D:\a\_work\1\s\src\fsharp\FSharp.Core\async.fs:line 447
at Microsoft.FSharp.Control.Trampoline.Execute(FSharpFunc`2 firstAction) in D:\a\_work\1\s\src\fsharp\FSharp.Core\async.fs:line 104
--- End of stack trace from previous location ---
at [email protected](ExceptionDispatchInfo edi) in D:\a\_work\1\s\src\fsharp\FSharp.Core\async.fs:line 1078
at Microsoft.FSharp.Control.Trampoline.Execute(FSharpFunc`2 firstAction) in D:\a\_work\1\s\src\fsharp\FSharp.Core\async.fs:line 104
at <StartupCode$FSharp-Core>[email protected](Object o) in D:\a\_work\1\s\src\fsharp\FSharp.Core\async.fs:line 183
at System.Threading.ThreadPoolWorkQueue.Dispatch()
at System.Threading.PortableThreadPool.WorkerThread.WorkerThreadStart()
at System.Threading.Thread.StartCallback()
Having the notion of the "top-level" messages is very confusing and to register a specific union case instead of the type of the DU is a bit weird.
Before talking about implementation details, lets look at things are now, here is my application specific message type on the client side:
type AppMsg =
// The messages coming from children
| PostsMsg of Posts.Types.Msg
| AdminMsg of Admin.Types.Msg
// the app messages
| LoadBlogInfo
| BlogInfoLoaded of Result<BlogInfo, string>
| BlogInfoLoadFailed of error:string
| UrlUpdated of Page
| NavigateTo of Page
// specialized message coming from server via Elmish.Bridge
| ServerMsg of RemoteServerMsg
| DoNothing
where RemoteServerMsg
is defines as
// Message from the server, telling the client to reload posts
type RemoteServerMsg =
| ReloadPosts
By registering the ServerMsg
as a mapping in the bridge client, bridge now has the responsibility of internally creating an AppMsg
of case ServerMsg
and sending it to the dispatch loop
My suggestion is as follows: let the user himself do the dispatch and mapping of messages whenever a message is coming from the server using a subscription, it would look this:
let remoteMsgSub =
Bridge.subscribe<RemoteServerMsg> (fun remoteServerMsg dispatch ->
// the user creates the AppMsg, not the library
let appMsg = AppMsg.ServerMsg remoteServerMsg
dispatch appMsg
)
This way, I could have more control and if I wanted, I could remove the ServerMsg of RemoteServerMsg
from the server entirely and dispatch some other message in my app that is specific to that subscription.
The same would happen on the server, the user subscribes to client messages and maps them himself.
What do you think of this? What challenges do you expect?
When relative endpoint location is specified in Program.withBridge
call, it is not relative during the runtime.
Consider the scenario, when app is developed locally as standalone - i.e. all routes are root-based, but deployed to IIS as a virtual app under some folder.
Here
the endpoint location with the existing path info is being replaced with the one passed with the bridge configuration.
Maybe it would be better to do this instead?
url.pathname <- url.pathname + this.path
Program.mkProgram Index.init Index.update Index.view
|> Program.withSubscription (fun model ->
let body msg msg_closed dispatch =
let config =
Bridge.endpoint $"{Url.learn_server}/{socket_endpoint}"
|> Bridge.withWhenDown msg_closed
|> Bridge.withMapping Index.FromServer
Bridge.asSubscription config dispatch
Bridge.Send // how do we use this?
config :> IDisposable
[
for KeyValue(name,pl) in model.cfr_players do
if pl.training_iterations_left > 0 then
let key = [ string name; "train"; string pl.training_iterations_left ]
key, body (FromClient (MsgClientToServer.Train (pl.training_iterations_left, pl.training_model)))
(Index.ConnectionClosed(name,true))
if pl.testing_iterations_left > 0 then
let key = [ string name; "test"; string pl.testing_iterations_left ]
key, body (FromClient (MsgClientToServer.Test (pl.testing_iterations_left, pl.testing_model)))
(Index.ConnectionClosed(name,false))
]
)
|> Program.withBridgeConfig (
Bridge.endpoint socket_endpoint
|> Bridge.withMapping Index.FromServer
)
|> Program.withReactSynchronous "app"
#if DEBUG
|> Program.withDebugger
#endif
|> Program.run
It is really good that Elmish.Bridge
has a way of closing and reacting to connections closings, but I want to be able to interact with multiple ws servers. I do not understand how Bridge.Send
could be configured to send to a specific server.
Is that possible?
Hello @Nhowka.
in one of the latest version of Fable.Browser.Url we fixed the URL.Create
.
The problem is that even if the new signature is in theory compatible with the previous one F#/.NET is not able to pick it when used in already published packages.
fable-compiler/fable-browser#116
Can you please update Elmish.Bridge.Clilent
to Fable.Browser.Url
version 1.3.2? In theory, you just need to update the dependency and have nothing to change in your code.
Building the client to .NET (i.e. not with Fable) I get an error:
The module/namespace 'Types' from compilation unit 'Browser.WebSocket' did not contain the val 'ValLinkagePartialKe(set_onclose)'
|> Program.withBridgeConfig ( Bridge.endpoint BridgeShared.endpoint |> Bridge.withMapping (ProjMgmt.Index.ServerMsg >> Users.Secure.InnerMsg) )
I can workaround with an #if FABLE_COMPILER guard. I like to compile without Fable to get intellisense working in Visual Studio. Once I do the proper build it works fine.
It appears that the new Json.serialize
serializes F#'s Lists in a way that is incompatible with what the server expects.
Remote message: ["Shared.Types.UpstreamMsg", "[\"UpstreamAppMsg\",[\"HwMsg\",[\"SwitchRelays\",{\"head\":[\"Pump1\",\"On\"],\"tail\":{\"head\":null}}]]]"]
[17:29:11 INF] Request finished HTTP/1.1 GET http://192.168.0.61:5000/appSock - - - 101 - - 1173352.2601ms
[17:29:12 INF] Request starting HTTP/1.1 GET http://192.168.0.61:5000/appSock - -
The other problem (besides messages not getting across) is that client simply gets disconnected and nobody reports any error, which makes it difficult to debug.
If this is outside the scope of your Vision of Elmish.Bride, please feel free to just close this ;)
I like the elmish concept in general, and I already use and like Elmish.Bridge for pushing data from server to client.
It is really good for one-way, message based communication.
But often you also need a RPC style, request-response flow, and that is a little bit cumbersome with just fire-and-forget messages.
I haven't thought too deeply about it, but what would you think about adding RPC capabilities by emulating some Actor models with a ReplyChannel abstraction?
The proposed API use would look very similar to the MailboxProcessor:
You have a ReplyChannel
class/interface.
On the serverside on the hub, you have AskClient
/ AskClientIf
, similar to BroadcastClient
/ SendClientIf
.
On the client side you have Bridge.Ask
, complimentary to Bridge.Send
.
IMO the Msg
and update
parts of elmish are already very similar to an actor, this would just extend this capability.
I am getting
[HPM] Error occurred while trying to proxy request /socket/init from localhost:8080 to http://localhost:8085 (ECONNREFUSED) (https://nodejs.org/api/errors.html#errors_common_system_errors)
[HPM] Error occurred while trying to proxy request /socket from localhost:8080 to http://localhost:8085 (ECONNREFUSED) (https://nodejs.org/api/errors.html#errors_common_system_errors)
[HPM] Error occurred while trying to proxy request /socket from localhost:8080 to http://localhost:8085 (ECONNREFUSED) (https://nodejs.org/api/errors.html#errors_common_system_errors)
[HPM] Error occurred while trying to proxy request /socket from localhost:8080 to http://localhost:8085 (ECONNREFUSED) (https://nodejs.org/api/errors.html#errors_common_system_errors)
indefinitely and elmish bridge does not work.
To reproduce:
1-) Create a new SAFE application with Elmish Bridge
2-) Update all packages via dotnet paket update (or just update Saturn, which in turn updates FSharp.Control.WebSockets and Microsoft.AspNetCore.WebSockets)
3-) Run the app.
On my server there is logic that works in parallel with the websocket Elmsih update. I found a method that allows you to get models of connected users:
connections.GetModels()
However, I can't find a method to send a message to the Elmish update
The following method is not suitable due to the fact that the message sent to it can also be sent from the client
connections.BroadcastServer (Inner Test)
The client has the following option
Bridge.Send (Inner Test)
A client app startup code looks like this:
Program.mkProgram init update render
|> Program.withBridgeConfig (Bridge.endpoint "./socket" |> Bridge.withUrlMode Append)
|> Program.withReactSynchronous "elmish-app"
|> Program.run
And for more advanced configuration I can use Calculated URL mode with a function that returns a url for socket communication.
But in both cases the url is resolved upfront. What if the configuration comes from an app settings JSON? Then the app typically introduces app settings load messages (e.g. LoadAppSettings and AppSettingsLoaded) and loads the settings asynchronously. The regular operations are deferred until app settings are loaded and the configuration is resolved.
It would be nice to be able to fit Bridge configuratoin into the same workflow i.e. defer setting up Bridge config URL resolution until the application settings are loaded. But it doesn't seem to be possible using withBridgeConfig. Or am I overlooking something?
In this section
open Elmish
open Elmish.Bridge
let server =
Bridge.mkServer Shared.endpoint init update
|> Bridge.run Giraffe.server
let webApp = // <---- ??? not referenced
choose [
server // <---- referenced twice?
route "/" >=> htmlFile "/pages/index.html" ]
let app =
application {
router server // <---- ??? use_router? server referenced twice?
disable_diagnostics
app_config Giraffe.useWebSockets
url uri
}
run app
Should that read instead use_router webApp
?
Trying to upgrade to Giraffe 6. Getting an error because it doesn't include Ply.
server: info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
server: Request starting HTTP/1.1 GET http://localhost:8080/socket/clientbridge - -
server: fail: Giraffe.Middleware.GiraffeErrorHandlerMiddleware[0]
server: An unhandled exception has occurred while executing the request.
server: System.IO.FileNotFoundException: Could not load file or assembly 'Ply, Version=0.3.1.0, Culture=neutral, PublicKeyToken=null'. The system cannot find the file specified.
server:
server: File name: 'Ply, Version=0.3.1.0, Culture=neutral, PublicKeyToken=null'
server: at [email protected](FSharpFunc`2 next, HttpContext ctx)
server: at [email protected]()
server: at [email protected]()
server: at [email protected]()
server: at [email protected]()
server: at Microsoft.AspNetCore.Session.SessionMiddleware.Invoke(HttpContext context)
server: at Microsoft.AspNetCore.Session.SessionMiddleware.Invoke(HttpContext context)
server: at Microsoft.AspNetCore.ResponseCompression.ResponseCompressionMiddleware.InvokeCore(HttpContext context)
server: at [email protected]()
server: info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
server: Request finished HTTP/1.1 GET http://localhost:8080/socket/clientbridge - - - 500 140 application/json;+charset=utf-8 1.3749ms
Workaround is to explicitly add nuget Ply
as a dependency.
Thoth.Json 3 is currently in beta.
When I tried to use Thoth.Json 3.0.0-beta-002 with Elmish.Bridge.Client 2.0.1, I got this error:
ERROR in ./.fable/Elmish.Bridge.Client.2.0.1/Library.fs
Module Error (from ./node_modules/fable-loader/index.js):
c:/Projekte/ebos/ebosYC/.fable/Elmish.Bridge.Client.2.0.1/Library.fs(106,18): (106,97) error FABLE: Cannot get type info of generic parameter Server, please inline or inject a type resolver
@ ./src/Client/App.fs 19:0-471 134:14-71 134:72-111 134:141-179 136:3-41 136:46-53 138:4-39
@ ./src/Client/Client.fsproj
@ multi ./src/Client/Client.fsproj ./src/Client/site.css
ERROR in ./.fable/Elmish.Bridge.Client.2.0.1/Library.fs
Module Error (from ./node_modules/fable-loader/index.js):
c:/Projekte/ebos/ebosYC/.fable/Elmish.Bridge.Client.2.0.1/Library.fs(97,23): (97,102) error FABLE: Cannot get type info of generic parameter Server, please inline or inject a type resolver
@ ./src/Client/App.fs 19:0-471 134:14-71 134:72-111 134:141-179 136:3-41 136:46-53 138:4-39
@ ./src/Client/Client.fsproj
@ multi ./src/Client/Client.fsproj ./src/Client/site.css
Trying to use a IReplyChannel<bool>
on the DotnetClient, but when using Bridge.AskServer
it has a compilation error:
The type 'bool' is not compatible with the type 'exn'
module TestReplyChannels =
type Msg =
| Foo of string * IReplyChannel<bool>
let _ = Bridge.AskServer (fun rc -> Foo("bar", rc))
Does AskServer somehow have an implicit requirement that 'T
inherits Exception?
Hi,
This is not really an issue report, but rather a personal question, so I'd like to excuse myself in advance for polluting this repository's issue tracker.
I've ported an existing app to Fable2 and in the process I had to find a new solution for Fable.Websockets.Elmish, so Elmish.Bridge seemed like a good solution.
It was only after much of the port was already completed, that I've found out that there's a slight change in the architecture...
My app has only one single "Elmish" state that is being shared and manipulated by all clients together, whereas Elmish.Bridge keeps a separate state for each client connection.
Now I'm trying to find an efficient way of recreating my original functionality (single state per app, rahter than one state per connection). Technically what I need to do is probably a single global mailbox processor into which all client's update
calls get diverted.
My question is that Elmish.Bridge already has a Mailbox procesor for each client connection so I'm wondering if there is a simple way to use or somehow plug into the existing code rather than wrapping another mailbox processor around existing ones.
I'll appreciate any help or hints on how to best approach this situation
(I'm not yet familiar with the internals of Elmish.Bridge -- if possible I'd rather plug into existing code rather than forking the whole repository).
Hi @Nhowka,
I have a question on fallbacks. Is there a plan to use / upgrade to https://github.com/aspnet/SignalR so that Elmish.Bridge
can take advantage of automatic fallbacks? According to https://docs.microsoft.com/en-us/aspnet/core/fundamentals/websockets?view=aspnetcore-2.1, using Microsoft.AspNetCore.WebSockets
requires the developer (aka either you or the app developer) to implement fallback to alternative transport protocols.
Would love to hear your thoughts on this.
Thank you
This project was born from the knowledge I got from working on Fable.Remoting. It's one of many amazing projects from @Zaid-Ajaj that I recommend if you don't know about it yet.
Now, it's time to leave the nest; the names are too similar and that will cause confusion.
On one hand, Fable.Remoting is a general use library. You can call functions over the wire and when done on the recommended guidelines is stateless. This project is designed to work on an Elmish environment. It's stateful and changes that state by exchanging messages between client and server (or server-server and client-client, but only the client-server travels through the magic channel WebSocket.
So. It's a client-server tool for communication with Elmish. Only Elmish. That is an important bit. The Remoting part? Well, I'll be sad to let it go, but we can live with another name. I even brought some options ready, but I'm not a creative person on naming things.
How about Elmish.Link
? Or maybe Elmish.Wire
? The project creates a way for messages to go on their way, traveling to change a state on a distant realm. (Or not that far, most action it got was on localhost.)
Enough of story time! Bring me your best names!
I have a scenario where I am using the Elmish.Bridge
architecture on the frontend with Elmish.Streams
. However this breaks the isomorphism, since Elmish.Bridge
on the server side uses Cmd
s by default, while Elmish.Streams
does not use Cmd
s.
You may know, Elmish.Streams
modifies the update
function, where instead of generating and returning Cmd
s there. There is a new function stream
that is subscribed to Program.mkSimple
and what would be done by update
with Cmd
s, is instead done in stream
, decoupled from update
.
What would be a simple way to use Elmish.Bridge
, modified to use Elmish.Streams
on the server side?
Using fable-compiler 3.4.10, all's fine with Fable.SimpleJson 3.5.0 - but with 3.6.0 or 3.7.0, I get a couple of build errors:
(Not a major problem: I can just stick with Fable.SimpleJson 3.5.0 for now. But thought it might be worth mentioning...)
I get the following when I reload the page with websockets with Chrome.
Unhandled exception. System.AggregateException: One or more errors occurred. (The WebSocket is in an invalid state ('Closed') for this operation. Valid states are: 'Open, CloseReceived')
---> System.Net.WebSockets.WebSocketException (0x80004005): The WebSocket is in an invalid state ('Closed') for this operation. Valid states are: 'Open, CloseReceived'
at System.Net.WebSockets.WebSocketValidate.ThrowIfInvalidState(WebSocketState currentState, Boolean isDisposed, WebSocketState[] validStates)
at System.Net.WebSockets.ManagedWebSocket.SendAsync(ReadOnlyMemory`1 buffer, WebSocketMessageType messageType, WebSocketMessageFlags messageFlags, CancellationToken cancellationToken)
--- End of inner exception stack trace ---
at [email protected](ExceptionDispatchInfo edi) in D:\a\_work\1\s\src\fsharp\FSharp.Core\async.fs:line 1078
at Microsoft.FSharp.Control.Trampoline.Execute(FSharpFunc`2 firstAction) in D:\a\_work\1\s\src\fsharp\FSharp.Core\async.fs:line 104
at <StartupCode$FSharp-Core>[email protected](Object o) in D:\a\_work\1\s\src\fsharp\FSharp.Core\async.fs:line 183
at System.Threading.ThreadPoolWorkQueue.Dispatch()
at System.Threading.PortableThreadPool.WorkerThread.WorkerThreadStart()
at System.Threading.Thread.StartCallback()
[16:52:00 INF] HTTP GET /socket responded 101 in 255.8078 ms
Is this just a simple check before the library calls SendAsync
to the WebSocket
if it's in valid state?
Is it possible to use this directly in javascript? Probably there should be a way to generate an api documentations for use with other clients?
As a consequence of how Program.withSubscription
is defined:
let withSubscription (subscribe : 'model -> Sub<'msg>) (program: Program<'arg, 'model, 'msg, 'view>) =
{ program with subscribe = subscribe }
The current implementation of Program.withBridge
and Program.withBridgeConfig
will overwrite any existing subscriptions. Alternatively, if there are subscriptions added after the bridge configuration, the Elmish.Bridge
subscriptions will be overwritten and their events will never be received.
So I'm suggesting the following additional functions to be added (bikeshedding welcome):
let inline withBridgeAndSubs
endpoint
(subs : 'model -> (string list * (('msg -> unit) -> IDisposable)) list)
(program : Program<_, 'model, 'msg, _>) =
program
|> Program.withSubscription
(fun model ->
subs model
@ [["Elmish";"Bridge"], fun dispatch -> let config = Bridge.endpoint(endpoint) in config.Attach dispatch; config ])
let inline withBridgeConfigAndSubs
(subs : 'model -> (string list * (('msg -> unit) -> IDisposable)) list)
(config:BridgeConfig<_,_>)
(program : Program<_, 'model, 'msg, _>) =
program
|> Program.withSubscription
(fun model ->
subs model
@ ["Elmish"::"Bridge"::(config.name |> Option.map List.singleton |> Option.defaultValue []), fun dispatch -> config.Attach dispatch; config])
So that the new style of Elmish
subscriptions can play nice with Elmish.Bridge
It seems that everything is moving ahead (including new versions of Saturn, Giraffe and Fable 3) and people are updating their software so things are starting to break.
Many recent starter projects, like https://github.com/Zaid-Ajaj/SAFE.Simplified will break dependencies that come with current version of Elmish.Bridge, because other packages are pulling in a more recent version of Giraffe:
Unhandled exception. System.IO.FileLoadException:
Could not load file or assembly 'Giraffe, Version=3.6.0.0, Culture=neutral, PublicKeyToken=null'.
The located assembly's manifest definition does not match the assembly reference. (0x80131040)
File name: 'Giraffe, Version=3.6.0.0, Culture=neutral, PublicKeyToken=null'
at Elmish.Bridge.Giraffe.server(String endpoint, FSharpFunc`2 inboxCreator)
at <StartupCode$Server>.$Program.main@()
Some people have already made necessary code fixes: https://github.com/Nhowka/Elmish.Bridge/network
Is there any chance of spinning a new version on Nuget?
According to Elmish release notes
https://github.com/elmish/elmish/blob/v4.x/RELEASE_NOTES.md
Breaking: withSubscription replaces existing subscription, use mapSubscription to add/accumulate the subscribers
when Elmish.Bridge is registered, any previously added subscription will be removed. I suggest migrating clients to mapSubscription
and aggregating all previous subs.
Cmd.bridgeSend
appears to trigger a runtime error:
I have published a repo that makes bug reproducible at: https://github.com/pkese/Elmish.Bridge.Test
To run:
Shell tab ./Elmish.Bridge.Test
-------------------------------------- --------------------------------------
> cd server > cd client
> dotnet restore > npm install
> dotnet run > ./run.sh
While processing Welcome
message it fails to serialize Cmd.bridgeSend UpstreamMsg.GetState
Apologies for even mentioning IE11 ...
We don't have a strong requirement for IE11, but it would be really nice to be able to still support it.
url-polyfill
If I set a breakpoint, remove the #
then continue, it works.
Do you have any idea where the hash comes from? Maybe it is an issue with url-polyfill
, I have no clue.
What do you think about just adding a TrimEnd('#')
?
EDIT: Same behavior with Edge (EdgeHTML 16.16299)
Elmish v4 has some changes with subscription, my fix locally looks like below
[<RequireQualifiedAccess>]
module Program =
/// Apply the `Bridge` to be used with the program.
/// Preferably use it before any other operation that can change the type of the message passed to the `Program`.
let inline withBridge endpoint (program : Program<_, _, _, _>) =
program |> Program.withSubscription (fun _ -> [["withBridge"],Bridge.endpoint(endpoint).Attach])
/// Apply the `Bridge` to be used with the program.
/// Preferably use it before any other operation that can change the type of the message passed to the `Program`.
let inline withBridgeConfig (config:BridgeConfig<_,_>) (program : Program<_, _, _, _>) =
program |> Program.withSubscription (fun _ -> ( [ ["withBridgeConfig"], config.Attach ]))
And return disposable from attach
When using several BroadcastClient(msg) in sequence, it seems that the client does not receive the messages in the same order. Is this intended? Is there a way to preserve the order?
After solving #40 and going about using AskServer with IReplyChannel, it took me a while to realise that using it seems to break other messages.
Using Bridge.AskServer with a reply channel works fine the first time, but only once, and after using it all other server messages stop being received by the client. The server can still receive messages sent by the client, though.
Also, usually if I stop the server, the client will print debug logs every second with the Bridge.withWhenDown
message. But after using a reply channel at least once, if I then stop the server, the client does not even get those connection down messages.
When I try to use elmish.bridge with 3.1, it fails with "initialization error", all packages are at the latest stable version. Using WSL on Win 10 x64
Related issue: elmish/elmish#206
I see the mappings are attached to dom tree, but not cleaned on dispose here:
Isn't this leaking, when I don't need elmish bridge?
Hi,
I'm testing RPC (Bridge.Client 6.0.2
) functionality along with Bridge.AskServer
function. To make it more comfortable to use, I thought about creating a helper like this:
let askServer f arg =
Bridge.AskServer(fun rc -> f(arg, rc))
// ... usage in an app
type ToServer =
| GetTodos of unit * IReplyChannel<Todo list>
type Msg =
| GotTodos of Todo list
| GotTodosError of exn
let cmd = Cmd.OfAsync.either
(Bridge.askServer GetTodos)
()
GotTodos GotTodosError
However, during runtime I receive an error:
TypeError: Cannot read properties of null (reading 'fullname')
(log attached)
error.log
Would you happen to have any idea what is the reason for this? Can it be fixed somehow?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.