Comments (22)
The first iteration uses a named query to know if the query function returns a new query or not.
query: 'model -> IAsyncObservable<'msg> -> INamedAsyncObservable<'msg>
There is a new operator called named: string -> INamedAsyncAbservable<'a>
that can be used to name your query.
let query model msgs =
match model.Counter with
| Some x ->
msgs |> named "messages"
| _ -> loadCountCmd |> named "initial"
To use the query with Elmish, use Program.withNameQuery
:
let withNamedQuery (query: 'model -> IAsyncObservable<'msg> -> INamedAsyncObservable<'msg>) (program: Elmish.Program<_,_,_,_>)
Current implementation at:
https://github.com/dbrattli/Fable.Reaction/blob/master/src/Program.fs#L52
from fable.reaction.
@MangelMaxime this should fix the problem with disposing timers, mouse-move subscriptions etc. when e.g. switching tabs.
from fable.reaction.
One thing I don't like about the INamedAsyncObservable<'msg>
is that it makes things a bit more confusing. What is a named observable? What/where is this named
operator that you have never heard about in other Rx impl? How do I unsubscribe (totally) without any new subscription (you would need to return just msgs
).
A more intuitive model could be to return a DU such as:
type Subscription<'msg> =
| Keep
| Dispose
| Create of IAsyncObservable<'msg>
... or:
type Subscription<'msg, 'key> =
| Keep
| Dispose
| Create of IAsyncObservable<'msg>*'key
We could choose to have Create
a tuple with a name to have same semantics as the named observable, thus if you return an observable with the same key as earlier, then it is the same as Keep
, but you don't need to impl. the logic to figure it out. Dispose
will dispose and unsubscribe any existing subscription. it would be equivalent of just returning msgs
since we don't want to stop messages dispatched by the view:
let query msgs =
msgs
We can take this further and make it even more advanced:
type Subscription<'msg, 'key> =
| Keep
| Dispose of 'key
| DisposeAll
| Create of IAsyncObservable<'msg>*'key
| ReplaceAll of IAsyncObservable<'msg>*'key
This would make it possible to setup multiple concurrent subscriptions for e.g sub-components, and you don't need to care about any logic (in the top most query) to merge all the streams from multiple sub-components together. Is this the shit, or am I over-engineering things?
What do you think @rommsen ?
from fable.reaction.
In really love the idea of using a DU here to create an easier to use API. Please give it a try
About the subcomponent. I really like the idea but how do you intend to use it to make it transparent for the parent?:
let query model msg =
match model.Page with
| NeedsWebsockets websocketModel ->
let subSubscription = WebsocketComponent.query msg websocketModel
// result is DisposeAll. how to go on from here?
// dont we need to analyze the result here to be able to know if the subscription should be disposed etc?
from fable.reaction.
Yes, it will not compose. The sub-page/component will need to just return msgs
(id-function) instead of dispose. But it needs a way to say that it has transformed the query in some way to trigger resubscribe. So it basically not a name, but a bool value or a DU with Changed/Unchanged of IAsyncObservable<'msg>
. This can be made composable so the if any sub-query changes then the total query will be Changed
triggering a resubscribe. I'll try to write an example later today.
from fable.reaction.
Going in circles here and dropping the Changed/Unchanged
idea, since it's hard for the query function to know if it has changed or not. Going back to the named observable idea, but making it simpler by returning a tuple IAsyncObservable<'msg>*string
and then generalizing that as IAsyncObservable<'msg>*'key
so you can use strings, integers or whatever that can be compared.
The withQuery
now looks like:
let withQuery (query: 'model -> IAsyncObservable<'msg> -> IAsyncObservable<'msg>*'key) (program: Elmish.Program<_,_,_,_>) =
And there is a helper for calling sub-queries for pages and componets that will help you with (un)wrapping to and from (sub-)messages.
let withSubQuery subquery submodel msgs wrapMsg unwrapMsg : IAsyncObservable<_> * string =
Thus a sub-query can be called like this:
let query (model: Model) (msgs: IAsyncObservable<Msg>) =
match model.PageModel with
| HomePageModel ->
msgs, "home"
...
| TomatoModel m ->
Program.withSubQuery Tomato.query m msgs TomatoMsg Msg.asTomatoMsg
The Msg.asTomatoMsg
is a helper function you can declare as an extension on Msg ('msg -> 'submsg option
). It takes a stream of messages and returns a stream of sub-messages e.g:
type Msg =
...
| TomatoMsg of Tomato.Msg
static member asTomatoMsg = function
| TomatoMsg tmsg -> Some tmsg
| _ -> None
Pages with multiple components (or Programs with concurrently active Pages) will need to compose the returned async observables from each sub-component together using e.g. AsyncObservable.merge
and join the keys to keep them unique, e.g. (+) for strings. We can make helper functions for this as well.
This will be the default withQuery
so the old way without model will be called withSimpleQuery
. API changes will bump the version to 3.
from fable.reaction.
Did you already publish the version to Nuget? Could not find it. Will try to reference the project directly to test it out.
from fable.reaction.
No nuget yet sorry. Will will fix this weekend. I will also publish an example project based on SAFE-BookStore that I used to get a larger example to play with.
from fable.reaction.
I played with it and so far I really like it (really need to get used to thinking in Streams, never done this before).
But I have a problem that I dont understand. I can also file a different issue or use another channel if you like to.
I created a playground for myself here: https://github.com/rommsen/Fable.Reaction.Playground (note I hardcoded the Fable.Reaction dependency in the project file to work with the latest version)
The idea is the following: there is a normal counter, when the counter is greater than 45 the time flies magic is started (locally), when the counter reaches 51 a websocket connection to the server is established and it is used for a message roundtrip. Everything seems to work nicely but it seems, that the Websocket connection is not disposed when the query returns a different key (e.g. counter is smaller than 51). Am I doing something wrong here or is there a bug somewhere in the implementation
from fable.reaction.
@rommsen I think there was a bug in the code so the websocket connection weren't closed on dispose. I've pushed a fix to master. Please try again.
from fable.reaction.
@dbrattli I changed my playground to work with the latest alpha and I tried to integrate subcomponents. So far so good (I still have problems to wrap my head around the rx stuff. Can I ask you questions about it? If yes which channel? Open tickets in here? Twitter message?).
I have one major problem when working with child components: When changing the query in one subcomponent the whole query of the app changes. I think that this is not feasible when working with "independent" sub components. In my example app (https://github.com/rommsen/Fable.Reaction.Playground) I am working with two subcomponents. Each of them has their own websocket connection (just to showcase the possibilities). When one subcomponent activates its WS connection, the connection of the other one is disposed and a new one is created. is IMHO this not what we want when we work with websockets, timer subscriptions or DOM events (to many websockets closed/opened, timer restarted etc.)
Is it somehow possible to compose different queries in a less destructive way? Maybe instead of dealing with a IAsyncObservable * key
tuple we could work with a triple: IAsyncObservable * identifier * key
and return a list of those triples from the original query. Then we could check each triple on its own if it has changed and dispose the changed one and reuse the others.
On a second thought this is not working right? Because in the end we need to merge the observables anyway into a new one and this is the one that has to be disposed, so we gain nothing.
Do you see any other way around this? I think that this might be a big showstopper when using subcomponents or just different composed observables in one query (I think the same problem happens here as well),
from fable.reaction.
@rommsen Yes, I think you have found a problem, and you are right about connections in one component should not dispose/reconnect in another.
The tricky thing is that we want a simple interface, and we also want processing of the query function to very fast since it's called every time the model changes. So we don't wan't the logic to be too complex.
For now you can try to setup 2 independent queries. Will that work for you?
Program.mkSimple init update view
|> Program.withQuery query1
|> Program.withQuery query2
We could also return a list of of IAsyncObservable * 'key
. I don't think we need the identifier since we could subscribe new keys, dispose missing keys and ignore existing keys (using Set module). That together with multiple queries should cover most scenaries since you in query2
may consume messages produces by query1
. There are some complicated issues if you e.g return 2 observables with different keys where one observable is composing the other. If the first contains a msgChannel then you will actually get 2 separate ws-connections (since there are 2 subscriptions). There are operators for fixing this using publish
/refCount
aka share
(currently not available in Reaction, but should be easy to impl).
from fable.reaction.
I was also playing around with lists of IAsyncObservable * 'key
but I did not got too far today.
I like the simplicity of multiple queries but I think there are two caveats in here.
- I think we need then three queries because we do not want to merge the normal msgs observable in both of them (to not get duplicated messages). So we get
let query1 (model: Model) (msgs: IAsyncObservable<Msg>) =
Program.withSubQuery Magic.query model.Magic msgs MagicMsg asMagicMsg
let query2 (model: Model) (msgs: IAsyncObservable<Msg>) =
Program.withSubQuery Info.query model.Info msgs InfoMsg asInfoMsg
let query3 (model: Model) (msgs: IAsyncObservable<Msg>) =
msgs,"default"
Program.mkSimple init update view
|> Program.withQuery query1
|> Program.withQuery query2
|> Program.withQuery query3
- this would basically stop at the first level of components (so no subcomponents of subcomponents. But I am not sure if we want this anyhow)
I would love to see your second scenario but I can not help too much because currently the operators you mentioned go far over my head.
from fable.reaction.
I have an (IAsyncObservable<'msg>*'key) list
version working and can send you an update during the day. But I got this crazy idea that what we are building is just some special kind of view
and that we should try to model it as a tree op : seq<prop> -> seq<IAsyncObservable<'msg> -> ??
so we can build it using familiar view
syntax i.e. opCode [] []
.
from fable.reaction.
@rommsen Have pushed Fable.Reaction-3.0.0-alpha3
that have a Program.withQueries
that you can try out. The signature looks like this:
let withQueries (query: 'model -> IAsyncObservable<'msg> -> (IAsyncObservable<'msg>*'key) list) (program: Elmish.Program<_,_,_,_>) =
from fable.reaction.
I have updated the version and changed my code to:
let queries (model: Model) (msgs: IAsyncObservable<Msg>) =
[
Program.withSubQuery Magic.query model.Magic msgs MagicMsg asMagicMsg
Program.withSubQuery Info.query model.Info msgs InfoMsg asInfoMsg
msgs,"none"
]
Program.mkSimple init update view
|> Program.withQueries queries
But now none of my subscriptions do work anymore.
Btw. is it possible that you did not push the changes to github? I tried to figure out why my subscriptions are broken but there is no push to Program.fs that contains withQueries
from fable.reaction.
Sorry about that. Pushed to Github now.
from fable.reaction.
I have sent a pr for the multi query approach. With this it works in general but I have still strange behaviour when using withQueries
instead of multiple withQuery
. It seems that not all messages go through. You can see this in my example project (when used with my PR). Normally one slider should be toggleable but now I have to use the other slider twice to get the same result
from fable.reaction.
Yes, also found the one in the PR. I found the other bugs as well. With withQueries
you should not merge msgs
in the subqueries such as below, as you will get duplicate messges (toggles off and on again immediately)
|> AsyncObservable.map (fun _ -> MsgAdded)
|> AsyncObservable.merge letterStringQuery
//|> AsyncObservable.merge msgs
, "remote"
Same in magic:
| Local letters ->
appModel.LetterString
|> letterStream
|> AsyncObservable.map Letter
//|> AsyncObservable.merge msgs
, (appModel.LetterString + "_local")
and
letterStringQuery
|> AsyncObservable.merge letterQuery
|> server
|> AsyncObservable.map RemoteMsg
//|> AsyncObservable.merge msgs
, (appModel.LetterString + "_remote")
In the same way you should not return msgs (since msgs is subscribed separately). So return an empty stream instead like this:
| _ ->
AsyncObservable.empty (),"none"
from fable.reaction.
Also if we change subquery to something like:
let subQuery subquery submodel msgs wrapMsg unwrapMsg : (IAsyncObservable<_> * string) list =
let msgs', name = subquery submodel (msgs |> AsyncObservable.choose unwrapMsg)
[(msgs' |> AsyncObservable.map wrapMsg, name)]
And the use yield!
, then we should be able to have sub(subQueries) as well. If I just could get the syntax a bit nicer.
from fable.reaction.
Cool, works now. I still have a question but I put this in the other repo: rommsen/Fable.Reaction.Playground#1
And the use yield!, then we should be able to have sub(subQueries) as well. If I just could get the syntax a bit nicer.
what do you think of something like
let queries (model: Model) (msgs: IAsyncObservable<Msg>) =
subscription {
query [msgs,"none"]
subquery Magic.query model.Magic msgs MagicMsg asMagicMsg
subquery Info.query model.Info msgs InfoMsg asInfoMsg
}
from fable.reaction.
I think we say this is good for now. Thanks for all the input.
from fable.reaction.
Related Issues (20)
- How to have query map a message for the current connection once and send the mapped message all connections HOT 1
- Websocket message channel filtering HOT 1
- Updating to Fable.Core 3.0 and friends HOT 8
- Update Awesome Fable links
- Messages based on Commands are not fed into stream HOT 11
- Need simpler example for Elmish.Streams.AspNetCore.Middleware HOT 4
- Fix Introduction in getting started HOT 1
- How to protect WS middleware with authentication? HOT 1
- Not compatible with netcoreapp3.1/netstandard2.1 target HOT 14
- Feliz update breaks Magic example HOT 9
- Reactive DOM engine based on AsyncRx? HOT 3
- Dispose subscription when observable terminates HOT 2
- Support disposing AsyncDisposable multiple times HOT 1
- Fable.Reaction on other elmish platforms HOT 3
- Async to IAsyncObservable? HOT 1
- AutoComplete example doesn't receive Msg.KeyboardEvent HOT 1
- Why Async.Start' ? HOT 3
- Usage with older Fable compiler HOT 4
- Documentation update
- Stream handling function does not obtain the actual state (it is always the initial one instead) HOT 5
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from fable.reaction.