fsprojects / FSharp.Data.Adaptive

On-demand adaptive/incremental data for F# https://fsprojects.github.io/FSharp.Data.Adaptive/
MIT License
250 stars 24 forks source link

A Changeable Value that can be depend on other adaptive values #114

Open patrickallensimpson opened 1 year ago

patrickallensimpson commented 1 year ago

My Team has been using the Fun.Blazor library to do a Blazor UI. In the process we've come across two scenarios in which we'd like a ChangeableValue to depend on another IAdaptiveValue.

In both cases we can definitely implement those capabilities with the current Library but they can be used interchangeably with cval. And it turns out that having the ability to represent changeabilty as type would be sort of nice.

So my question I think it might be useful to to have either an IChangeableValue that derives from IAdaptiveValue and provides the hooks so that code that sees cval can work with any particular implementation or unseal ChangeableValue to allow extension for the same reason. I think the IChangeableValue might be the better route but I wanted some feedback about the suggestion.

Another type we also implemented for representing Async computations that might need to be refreshed after you use them (ex. Database queries with the same arguments):

    type AsyncStatus<'T> =
    | Init
    | Ready of data:'T
    | Loading of msg:string*elapsed:System.TimeSpan
    | Error of msg:string*when':System.DateTimeOffset
    [<StructuredFormatDisplay("{AsString}")>]
    type AsyncChangeableValue<'T>(async':'T Async,?loadingMessage:string,?requireRefresh:bool) as this =
        inherit AdaptiveObject()

        // the initial value is AsyncStatus<'T>.Init
        // thier is also a quiet cache used to attempt refreshing without triggering a change notification until finished

        member x.Value with get                
        member x.GetValue (token: AdaptiveToken) : AsyncStatus<'T>
        /// the async version allows the user to await until the operation is completed to continue with another operation.
        /// if a RefreshQuiet was in progress then it is turned into a normal Refresh operation
        /// if a RefreshAsync in already in flight then this returns an Async<unit> that can be waiting on until the original operation
        /// complete
        /// This cause a Refresh to immediately start and the returned Async<unit> is only used to synchronize with the results. Think Async.StartAsChild(...)
        member x.RefreshAsync() : Async<unit>
        /// this version triggers a refresh and then immediately returns (fire and forget), internally it fires a RefreshAsync and then ignore the resulting Async<unit>
        member x.Refresh() : unit
        /// will cause a refresh of the async computation but will NOT change the state of the cached data
        /// unless it successfully completes. updateOnlyWhenChanged will execute an expensive comparison on
        /// the incoming value to only mark this aval as changed if it is not equal to the current value
        /// ignoreErrors will NOT cause a Change in value if an Error occurs, instead the current value is retained
        /// if a RefreshAsync was in progress then this returns an Async<unit> that can be waited on until the original operation is complete
        /// if a RefreshQuietAsync in already in flight then this returns an Async<unit> that can be waiting on until the original operation
        /// completes
        /// This cause a Refresh to immediately start and the returned Async<unit> is only used to synchronize with the results. Think Async.StartAsChild(...)
        member x.RefreshQuietAsync([<Optional;DefaultParameterValue(false)>]ignoreErrors,[<Optional;DefaultParameterValue(false)>]updateOnlyWhenChanged) : Async<unit>
        /// triggers a RefreshQuietAsync operation and then immediately returns, ignoring the Async<unit> result of RefreshQuiet.
        member x.RefreshQuiet([<Optional;DefaultParameterValue(false)>]ignoreErrors) = x.RefreshQuietAsync(ignoreErrors) |> Async.Start
        member result.MapWithDefault (mapping:'T -> 'U,?defaultValue:'U) : aval<'U>
        member result.IsErrored : aval<bool>
        member result.IsLoading : aval<bool>
        member result.IsReady : aval<bool>
        member result.ReadyValue : aval<'T option>
        member result.LoadingMessage : aval<(string * System.TimeSpan) option>
        member result.ErrorMessage : aval<(string * System.DateTimeOffset) option>

        interface IAdaptiveValue with
            ...

        interface IAdaptiveValue<DataStatus<'T>> with
            ...

        member private x.AsString = sprintf "acval(%A)" x.Value
        override x.ToString() = System.String.Format("acval({0})", x.Value)

    and acval<'T> = AsyncChangeableValue<'T>

Thanks in advance for any comments and suggestions.

-Patrick

I just saw this discussion which did discuss something like the AsyncChangeableValue type above: https://github.com/fsprojects/FSharp.Data.Adaptive/discussions/101#discussioncomment-2614410