Giter VIP home page Giter VIP logo

Comments (22)

dbrattli avatar dbrattli commented on June 2, 2024

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.

dbrattli avatar dbrattli commented on June 2, 2024

@MangelMaxime this should fix the problem with disposing timers, mouse-move subscriptions etc. when e.g. switching tabs.

from fable.reaction.

dbrattli avatar dbrattli commented on June 2, 2024

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.

rommsen avatar rommsen commented on June 2, 2024

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.

dbrattli avatar dbrattli commented on June 2, 2024

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.

dbrattli avatar dbrattli commented on June 2, 2024

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.

rommsen avatar rommsen commented on June 2, 2024

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.

dbrattli avatar dbrattli commented on June 2, 2024

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.

rommsen avatar rommsen commented on June 2, 2024

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.

dbrattli avatar dbrattli commented on June 2, 2024

@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.

rommsen avatar rommsen commented on June 2, 2024

@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.

dbrattli avatar dbrattli commented on June 2, 2024

@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.

rommsen avatar rommsen commented on June 2, 2024

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.

  1. 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
  1. 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.

dbrattli avatar dbrattli commented on June 2, 2024

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.

dbrattli avatar dbrattli commented on June 2, 2024

@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.

rommsen avatar rommsen commented on June 2, 2024

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.

dbrattli avatar dbrattli commented on June 2, 2024

Sorry about that. Pushed to Github now.

from fable.reaction.

rommsen avatar rommsen commented on June 2, 2024

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.

dbrattli avatar dbrattli commented on June 2, 2024

Yes, also found the one in the PR. I found the other bugs as well. With withQueriesyou 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.

dbrattli avatar dbrattli commented on June 2, 2024

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.

rommsen avatar rommsen commented on June 2, 2024

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.

dbrattli avatar dbrattli commented on June 2, 2024

I think we say this is good for now. Thanks for all the input.

from fable.reaction.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.